/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
package org.apache.empire.jsf2.pageelements;

import java.io.Serializable;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;

import javax.faces.event.ActionEvent;

import org.apache.empire.commons.ObjectUtils;
import org.apache.empire.exceptions.NotSupportedException;
import org.apache.empire.exceptions.ObjectNotValidException;
import org.apache.empire.jsf2.pages.Page;
import org.apache.empire.jsf2.pages.PageElement;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public abstract class ListPageElement<T> extends PageElement
{
    private static final long   serialVersionUID = 1L;

    private static final Logger log              = LoggerFactory.getLogger(ListPageElement.class);

    protected Class<T>          beanClass;

    protected List<T>           items            = null;

    protected SelectionSet      selectedItems    = null;
    
    public static class SelectionSet extends HashSet<String>
    {
        private static final long serialVersionUID = 1L;
        
        private boolean singleSelection = false;
        private boolean invertSelection = false;
        
        public SelectionSet()
        {
            super();
        }
        
        public SelectionSet(int size)
        {
            super(size);
        }
    
        public boolean isSingleSelection()
        {
            return singleSelection;
        }

        public void setSingleSelection(boolean singleSelection)
        {
            this.singleSelection = singleSelection;
            clear();
        }

        public boolean isInvertSelection()
        {
            return invertSelection;
        }

        public boolean set(String e)
        {
            clear();
            return super.add(e);
        }

        @Override
        public boolean add(String e)
        {
            if (singleSelection)
                return false;
            return super.add(e);
        }

        @Override
        public boolean remove(Object o)
        {
            if (singleSelection)
                return false;
            return super.remove(o);
        }

        public void setInvertSelection(boolean invertSelection)
        {
            if (this.invertSelection==invertSelection)
                return; // no change
            // invert 
            this.invertSelection = invertSelection;
            this.clear();
        }
        
        @Override
        public boolean contains(Object item)
        {
            boolean exists = super.contains(item);
            return (invertSelection ? !exists : exists);
        }
    }

    /**
     * Interface to uniquely identify a list item
     * This will be used for selection and navigation 
     */
    public static interface ParameterizedItem {

        public String getIdParam();
        
        public void setIdParam(String idParam);
    }
    
    /**
     * Abstract superclass to make a list item selectable
     */
    public static abstract class SelectableItem implements Serializable
    {
        private static final long serialVersionUID = 1L;

        private SelectionSet      selectSet        = null;

        protected void setSelectMap(SelectionSet selectSet)
        {
            this.selectSet = selectSet;
        }

        public boolean isSelected()
        {
            String id = getIdParam();
            return this.selectSet.contains(id);
        }

        public void setSelected(boolean selected)
        {
            String id = getIdParam();
            if (selected != this.selectSet.isInvertSelection())
            {
                this.selectSet.add(id);
            }
            else
            {
                this.selectSet.remove(id);
            }
        }

        public abstract String getIdParam();

    }

    /**
     * This class holds information about the list view to display.
     * This will be held on the session in order to maintain position and sorting when navigating back and forth. 
     */
    public static class ListTableInfo implements Serializable
    {
        private static final long serialVersionUID = 1L;

        /** Pagination **/
        /**
         * position, starts with 0
         */
        private int               itemCount        = -1;
        private boolean           valid            = false;
        private boolean           modified         = false;

        /** Sorting **/
        private String            sortColumnName;
        private boolean           sortAscending    = true;
        private boolean           sortOrderChanged = false;

        /** Pagination **/
        private int               position         = 0;
        private int               pageSize         = 0;

        public void init(int itemCount, int pageSize)
        {
            if (pageSize < 0)
            {   // pageSize must not be negative!
                throw new ObjectNotValidException(this);
            }
            this.itemCount = itemCount;
            this.pageSize = pageSize;
            this.position = 0;
            this.valid = (itemCount >= 0);
            this.modified = false;
        }

        public boolean isValid()
        {
            return this.valid;
        }

        public void setValid(boolean valid)
        {
            if (valid && (this.itemCount < 0))
            { // itemCount and position must not be negative!
                throw new ObjectNotValidException(this);
            }
            this.valid = valid;
        }

        public boolean isModified()
        {
            return (this.valid && this.modified);
        }

        public void setModified(boolean modified)
        {
            this.modified = modified;
        }

        public int getItemCount()
        {
            return this.itemCount;
        }

        public int getPageSize()
        {
            return this.pageSize;
        }

        /*** sorting ***/

        public String getSortColumnName()
        {
            return this.sortColumnName;
        }

        public void setSortColumnName(String column)
        {
            if (ObjectUtils.compareEqual(column, this.sortColumnName))
            {
                return;
            }
            // change value
            this.sortColumnName = column;
            this.position = 0;
            this.modified = true;
            this.sortOrderChanged = true;
        }

        public boolean getSortAscending()
        {
            return this.sortAscending;
        }

        public void setSortAscending(boolean sortAscending)
        {
            if (this.sortAscending == sortAscending)
            {
                return;
            }
            // change value
            this.sortAscending = sortAscending;
            this.modified = true;
            this.sortOrderChanged = true;
        }

        public boolean isSortOrderChanged()
        {
            return this.sortOrderChanged;
        }

        public void setSortOrderChanged(boolean sortOrderChanged)
        {
            this.sortOrderChanged = sortOrderChanged;
        }

        /*** pagination ***/

        public int getPosition()
        {
            return this.position;
        }

        public void setPosition(int position)
        {
            if (this.pageSize == 0)
            {
                throw new NotSupportedException(this, "setPosition");
            }
            if (position < 0)
            {
                position = 0;
            }
            if (position >= this.itemCount)
            {
                position = this.itemCount - 1;
            }
            if (this.position == position)
            {
                return;
            }
            // change value
            this.position = position;
            this.modified = true;
        }

        public boolean isAllowPagination()
        {
            return (this.pageSize > 0);
        }

        public boolean isHasNextPage()
        {
            if (isValid() && isAllowPagination())
            {
                return ((this.itemCount - (this.position + this.pageSize)) > 0);
            }
            return false;
        }

        public boolean isHasPrevPage()
        {
            return (isValid() && isAllowPagination() && this.position > 0);
        }

        /**
         * set the next x entries
         */
        public void nextPage(ActionEvent e)
        {
            if (!isAllowPagination())
            {
                throw new NotSupportedException(this, "nextPage");
            }
            // Check
            if ((this.position + this.pageSize) > this.itemCount)
            {
                return; // Already on last page
            }
            // One page forward
            this.position += this.pageSize;
            this.modified = true;
        }

        /**
         * set the prev x entries
         */
        public void prevPage(ActionEvent e)
        {
            if (!isAllowPagination())
            {
                throw new NotSupportedException(this, "prevPage");
            }
            // Check
            if (this.position == 0)
            {
                return; // Already on first page
            }
            // one page back
            if (this.position > this.pageSize)
            {
                this.position -= this.pageSize;
            }
            else
            {
                this.position = 0;
            }
            // modified
            this.modified = true;
        }

        public int getPageNumber()
        {
            if (!isAllowPagination())
            {
                return 1; // always first page
            }
            // Calc Page num
            int pageNumber = new Double(Math.ceil(((double) this.position / this.pageSize))).intValue() + 1;
            return pageNumber;
        }

        public int getPageCount()
        {
            if (!isAllowPagination())
            {
                return 1; // only one page
            }
            // Calc Page count
            int pageCount = new Double(Math.ceil(((double) this.itemCount / this.pageSize))).intValue();
            return pageCount;
        }

    }

    public ListPageElement(Page page, Class<T> beanClass, String propertyName)
    {
        super(page, propertyName);
        // set bean Class
        this.beanClass = beanClass;
        // selectable
        if (ListPageElement.isSelectableItem(beanClass))
        {
            this.selectedItems = new SelectionSet();
        }
    }

    private static <T> boolean isSelectableItem(Class<T> beanClass)
    {
        Class<?> superClass = beanClass.getSuperclass();
        while (superClass != null)
        { // Check Superclass
            if (superClass.equals(SelectableItem.class))
            {
                return true;
            }
            superClass = superClass.getSuperclass();
        }
        return false;
    }

    /** session scoped properties **/
    public abstract ListTableInfo getTableInfo();
    
    public List<T> getItems()
    {
        if (this.items == null)
        {
            ListPageElement.log.warn("Bean List has not been initialized!");
        }
        return this.items;
    }

    /**
     * returns whether the item list has been loaded 
     * @return true if the item list has been loaded even it contains no items for false otherwise
     */
    public final boolean isValid()
    {
        return (this.items != null);
    }

    /**
     * returns true if the either the item list is not valid or if it contains no items 
     * @return true if the either the item list is not valid or if it contains no items
     */
    public final boolean isEmpty()
    {
        return (this.items==null || this.items.size()==0);
    }

    /**
     * returns whether the item list is valid and contains at least one item 
     * @return true if the item list contains one or more items or false otherwise
     */
    public final boolean isNotEmpty()
    {
        return (this.items!=null && this.items.size()>0);
    }

    /**
     * added as "isEmpty" is not accessible from EL.
     * @return true if the either the item list is not valid or if it contains no items
     */
    public final boolean isBlank()
    {
        return isEmpty();
    }

    /**
     * returns the total item count of the entire list (not just the visible part)
     * @return the total number of items in the list
     */
    public int getItemCount()
    {
        return getTableInfo().getItemCount();
    }

    public void clearItems()
    {
        clearSelection();
        this.items = null;
    }

    /*** Selection ***/

    public void clearSelection()
    {
        if (this.selectedItems != null)
        {
            this.selectedItems.clear();
        }
    }

    public boolean isHasSelection()
    {
        if (this.selectedItems==null)
            return false;
        // Has selected Items
        return (selectedItems.size()>0 || selectedItems.isInvertSelection());
    }

    public int getSelectedItemCount()
    {
        if (this.selectedItems==null)
            return 0;
        // Item count
        return this.selectedItems.size();
    }

    public List<T> getSelectedItems()
    {
        if (this.selectedItems==null)
            throw new NotSupportedException(this, "getSelectedItems");
        // find all items
        List<T> selection = new ArrayList<T>(this.selectedItems.size());
        for (T item : getItems())
        {
            if (((SelectableItem)item).isSelected())
                selection.add(item);
        }
        return selection;
    }
    
    public boolean isInvertSelection()
    {
        if (this.selectedItems==null)
            return false;
        // Invert selection
        return this.selectedItems.isInvertSelection();
    }

    public void setInvertSelection(boolean invertSelection)
    {
        if (this.selectedItems==null)
            throw new NotSupportedException(this, "setInvertSelection");
        // Invert
        this.selectedItems.setInvertSelection(invertSelection);
    }
    
    public boolean isSingleSelection()
    {
        if (this.selectedItems==null)
            return false;
        // Invert selection
        return this.selectedItems.isSingleSelection();
    }

    public void setSingleSelection(boolean singleSelection)
    {
        if (this.selectedItems==null)
            throw new NotSupportedException(this, "setSingleSelection");
        // Invert
        this.selectedItems.setSingleSelection(singleSelection);
    }

    public void setSelection(SelectableItem item)
    {
        if (this.selectedItems==null)
            throw new NotSupportedException(this, "setInvertSelection");
        // Invert
        if (item!=null)
            this.selectedItems.set(item.getIdParam());
        else
            this.selectedItems.clear();
    }

    public void setSelection(SelectableItem[] items)
    {
        if (this.selectedItems==null)
            throw new NotSupportedException(this, "setInvertSelection");
        // Invert
        this.selectedItems.clear();
        for (SelectableItem item : items)
            this.selectedItems.add(item.getIdParam());
    }

    protected void assignSelectionMap(List<?> items)
    {
        // Check selectable
        if (this.selectedItems == null)
            return;
        // generate all
        for (Object item : items)
        {
            if (item instanceof SelectableItem)
            {
                ((SelectableItem) item).setSelectMap(this.selectedItems);
            }
        }
    }
}
