/*
 * Created on Nov 23, 2004
 * 
 *  This file is part of susimail project, see http://susi.i2p/
 *  
 *  Copyright (C) 2004-2005  <susi23@mail.i2p>
 *
 *  This program is free software; you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation; either version 2 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program; if not, write to the Free Software
 *  Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 *  
 *  $Revision: 1.2 $
 */
package i2p.susi.util;


import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import net.i2p.data.DataHelper;

/**
 * Folder object manages a array Object[] to support
 * paging and sorting.
 * 
 * You create a folder object, set the contents with setElements(),
 * add Comparators with addSorter(), choose one with setSortBy() and
 * and then fetch the content of the current page with
 * currentPageIterator().
 * 
 * All public methods are synchronized.
 * 
 * @author susi
 */
public class Folder<O extends Object> {
	
	public static final String PAGESIZE = "pager.pagesize";
	public static final int DEFAULT_PAGESIZE = 10;

	public enum SortOrder {
		/** lowest to highest */
		DOWN,
		/** reverse sort, highest  to lowest */
		UP;
	}

	/**
	 * @since 0.9.63
	 */
	public interface Selector<O> {
		/**
		 * @return non-null
		 */
		public String getSelectionKey();
		/**
		 * @return true if selected
		 */
		public boolean select(O element);
	}

	private int pages, pageSize, currentPage;
	private O[] elements;
	private final Map<String, Comparator<O>> sorter;
	private SortOrder sortingDirection;
	private Comparator<O> currentSorter;
	private String currentSortID;
	private Selector<O> currentSelector;
	private List<O> selected;
	
	public Folder()
	{
		pages = 1;
		currentPage = 1;
		sorter = new HashMap<String, Comparator<O>>();
		sortingDirection = SortOrder.DOWN;
	}
	
	/**
	 * Returns the current page.
	 * Starts at 1, even if empty.
	 * 
	 * @return Returns the current page.
	 */
	public synchronized int getCurrentPage() {
		return currentPage;
	}

	/**
	 * Sets the current page to the given parameter.
	 * Starts at 1.
	 * 
	 * @param currentPage The current page to set.
	 */
	public synchronized void setCurrentPage(int currentPage) {
		if( currentPage >= 1 && currentPage <= pages )
			this.currentPage = currentPage;
	}

	/**
	 * Returns the size of the folder.
	 * 
	 * @return Returns the size of the folder.
	 */
	public synchronized int getSize() {
		return elements != null ? elements.length : 0;
	}

	/**
	 * Returns the number of pages in the folder.
         * Minimum of 1 even if empty.
	 * If the selector is non-null, this will be the number of pages in the selected results.
	 *
	 * @return Returns the number of pages.
	 */
	public synchronized int getPages() {
		if (currentSelector == null || elements == null)
			return pages;
		int ps = getPageSize();
		int rv = selected.size() / ps;
		if (rv * ps < elements.length)
			rv++;
		return rv;
	}

	/**
	 * Returns page size. If no page size has been set, it returns property @link PAGESIZE.
	 * If no property is set @link DEFAULT_PAGESIZE is returned.
	 * 
	 * @return Returns the pageSize.
	 */
	public synchronized int getPageSize() {
		return pageSize > 0 ? pageSize : Config.getProperty( PAGESIZE, DEFAULT_PAGESIZE );
	}

	/**
	 * Set page size.
	 * 
	 * @param pageSize The page size to set.
	 */
	public synchronized void setPageSize(int pageSize) {
		if( pageSize > 0 )
			this.pageSize = pageSize;
		update();
	}

	/**
	 * Recalculates variables.
	 */
	private void update() {
		if( elements != null ) {
			pages = elements.length / getPageSize();
			if( pages * getPageSize() < elements.length )
				pages++;
			if( currentPage > pages )
				currentPage = pages;
		}
		else {
			pages = 1;
			currentPage = 1;
		}
	}

	/**
	 * Sorts the elements according the order given by @link addSorter()
	 * and @link setSortBy().
         *
         * @since public since 0.9.33
	 */
	public synchronized void sort()
	{
		if (currentSorter != null && elements != null && elements.length > 1)
			DataHelper.sort(elements, currentSorter);
	}
	
	/**
	 * Set the array of objects the folder should manage.
	 * Does NOT copy the array.
	 * Sorts the array if a sorter set.
	 * 
	 * @param elements Array of Os.
	 */
	private synchronized void setElements( O[] elements )
	{
		if (elements.length > 0) {
			this.elements = elements;
			sort();
			select();
		} else {
			this.elements = null;
		}
		update();
	}

	/**
	 * Remove an element
	 * 
	 * @param element to remove
	 */
	public void removeElement(O element) {
		removeElements(Collections.singleton(element));
	}
	
	/**
	 * Remove elements
	 * 
	 * @param elems to remove
	 */
	@SuppressWarnings("unchecked")
	public synchronized void removeElements(Collection<O> elems) {
		if (elements != null) {
			List<O> list = new ArrayList<O>(Arrays.asList(elements));
			boolean shouldUpdate = false;
			for (O e : elems) {
				if (list.remove(e))
					shouldUpdate = true;
			}
			if (shouldUpdate) {
				elements = (O[]) list.toArray(new Object[list.size()]);
				update();  // will still be sorted
			}
		}
	}

	/**
	 * Add an element only if it does not already exist
	 * 
	 * @param element to add
	 * @return true if added
	 */
	public boolean addElement(O element) {
		return addElements(Collections.singletonList(element)) > 0;
	}
	
	/**
	 * Add elements only if they do not already exist
	 * Re-sorts the array if a sorter is set and any elements are actually added.
	 * 
	 * @param elems to add
	 * @return number added
	 */
	@SuppressWarnings("unchecked")
	public synchronized int addElements(List<O> elems) {
		int added = 0;
		if (elements != null) {
			// delay copy until required
			List<O> list = null;
			for (O e : elems) {
				boolean found = false;
				for (int i = 0; i < elements.length; i++) {
					if (e.equals(elements[i])) {
						found = true;
						break;
					}
				}
				if (!found) {
					if (list == null) {
						list = new ArrayList<O>(Arrays.asList(elements));
					}
					list.add(e);
				}
			}
			if (list != null) {
				added = list.size() - elements.length;
				setElements((O[]) list.toArray(new Object[list.size()]));
			}
		} else if (!elems.isEmpty()) {
			added = elems.size();
			setElements((O[]) (elems.toArray(new Object[added])));
		}
		return added;
	}
	
	/**
	 * Returns an iterator containing the elements on the current page.
         * This iterator is over a copy of the current page, and so
         * is thread safe w.r.t. other operations on this folder,
         * but will not reflect subsequent changes, and iter.remove()
         * will not change the folder.
         *
	 * @return Iterator containing the elements on the current page.
	 */
	public synchronized Iterator<O> currentPageIterator()
	{
		ArrayList<O> list = new ArrayList<O>();
		if( elements != null ) {
			int pageSize = getPageSize();
			int offset = ( currentPage - 1 ) * pageSize;
			for( int i = 0; i < pageSize && offset >= 0 && offset < elements.length; i++ ) {
				list.add( elements[offset] );
				offset++;
			}			
		}
		return list.iterator();
	}

	/**
	 * Returns an iterator containing the elements on the current page.
	 * This iterator is over a copy of the current page, and so
	 * is thread safe w.r.t. other operations on this folder,
	 * but will not reflect subsequent changes, and iter.remove()
	 * will not change the folder.
	 *
	 * @return Iterator containing the elements on the current page.
	 * @since 0.9.63
	 */
	public synchronized Iterator<O> currentPageSelectorIterator()
	{
		if (currentSelector == null)
			return currentPageIterator();
		int pageSize = getPageSize();
		int offset = ( currentPage - 1 ) * pageSize;
		if (selected == null || offset > selected.size())
			return Collections.<O>emptyList().iterator();
		return selected.subList(offset, Math.min(selected.size(), offset + pageSize)).iterator();
	}

	/**
	 * Turns folder to next page.
	 */
	public synchronized void nextPage()
	{
		currentPage++;
		if( currentPage > pages )
			currentPage = pages;
	}
	
	/**
	 * Turns folder to previous page.
	 */
	public synchronized void previousPage()
	{
		currentPage--;
		if( currentPage < 1 )
			currentPage = 1;		
	}
	
	/**
	 * Sets folder to display first page.
	 */
	public synchronized void firstPage()
	{
		currentPage = 1;
	}
	
	/**
	 * Sets folder to display last page.
	 */
	public synchronized void lastPage()
	{
		currentPage = pages;
	}
	
	/**
	 * Adds a new sorter to the folder. You can sort the folder by
	 * calling setSortBy() and choose the given id there.
	 * 
	 * @param id ID to identify the Comparator with @link setSortBy()
	 * @param sorter a Comparator to sort the Array given by @link setElements()
	 */
	public synchronized void addSorter( String id, Comparator<O> sorter )
	{
		this.sorter.put( id, sorter );
	}
	
	/**
	 * Activates sorting by the choosen Comparator. The id must
	 * match the one, which the Comparator has been stored in the
	 * folder with @link addSorter().
	 * Sets the sorting direction of the folder.
	 * 
	 * Warning, this does not do the actual sort, only addElements() and setElements() does a sort.
	 * 
	 * @param id ID to identify the Comparator stored with @link addSorter()
	 * @param direction UP or DOWN. UP is reverse sort.
	 */
	public synchronized void setSortBy(String id, SortOrder direction)
	{
		SortOrder oldDirection = sortingDirection;
		sortingDirection = direction;
		Comparator<O> oldSorter = currentSorter;
		currentSorter = sorter.get( id );
		if (currentSorter != null) {
			if (sortingDirection == SortOrder.UP)
				currentSorter = Collections.reverseOrder(currentSorter);
			currentSortID = id;
		} else {
			currentSortID = null;
		}
		// invalidate selection if sort order changed
		if (oldDirection != sortingDirection || !DataHelper.eq(oldSorter, currentSorter)) {
			currentSelector = null;
			selected = null;
		}
	}
	
	/**
	 * @since 0.9.13
	 */
	public synchronized String getCurrentSortBy() {
		return currentSortID;
	}
	
	/**
	 * @since 0.9.13
	 */
	public synchronized SortOrder getCurrentSortingDirection() {
		return sortingDirection;
	}

	/**
	 * Warning, this does not do the actual selection, this is done in the iterator.
	 * Resets page to 1 if selector changed.
	 *
	 * @param selector may be null
	 * @since 0.9.63
	 */
	public synchronized void setSelector(Selector<O> selector) {
		if ((currentSelector != null && selector == null) ||
		    (currentSelector == null && selector != null) ||
		    (currentSelector != null && selector != null &&
		     !currentSelector.getSelectionKey().equals(selector.getSelectionKey()))) {
			currentPage = 1;
		}
		currentSelector = selector;
		if (selector != null)
			select();
		else
			selected = null;
	}

	/**
	 * @return current selector or null
	 * @since 0.9.63
	 */
	public synchronized Selector<O> getCurrentSelector() {
		return currentSelector;
	}

	/**
	 * Select and cache results
	 * @since 0.9.63
	 */
	private synchronized void select() {
		if (selected == null)
			selected = new ArrayList<O>();
		else
			selected.clear();
		if (elements == null || currentSelector == null)
			return;
		int sz = getSize();
		for (int i = 0; i < sz; i++) {
			if (currentSelector.select(elements[i])) {
				selected.add(elements[i]);
			}
		}
	}

	/**
	 * Returns the first element of the sorted folder.
	 * 
	 * @return First element.
	 */
	public synchronized O getFirstElement()
	{
		return elements == null ? null : getElement( 0 );
	}

	/**
	 * Returns the last element of the sorted folder.
	 * 
	 * @return Last element.
	 */
	public synchronized O getLastElement()
	{
		return elements == null ? null : getElement(  elements.length - 1 );
	}
	
	/**
	 * Gets index of an element in the array regardless of sorting direction.
	 * 
	 * @param element
	 * @return index
	 */
	private int getIndexOf( O element )
	{
		if( elements != null ) {
			for( int i = 0; i < elements.length; i++ )
				if( elements[i].equals( element ) )
					return i;
		}
		return -1;
	}
	
	/**
	 * Retrieves the next element in the sorted array.
	 * 
	 * @param element
	 * @return The next element
	 */
	public synchronized O getNextElement( O element )
	{
		O result = null;
		
		int i = getIndexOf( element );

		if( i != -1 && elements != null ) {
			i++;
			if( i >= 0 && i < elements.length )
				result = elements[i];
		}
		return result;
	}
	
	/**
	 * Retrieves the previous element in the sorted array.
	 * 
	 * @param element
	 * @return The previous element
	 */
	public synchronized O getPreviousElement( O element )
	{
		O result = null;
		
		int i = getIndexOf( element );

		if( i != -1 && elements != null ) {
			i--;
			if( i >= 0 && i < elements.length )
				result = elements[i];
		}
		return result;
	}

	/**
	 * Retrieves the next element in the sorted array.
	 * 
	 * @param element
	 * @return The next element
	 * @since 0.9.63
	 */
	public synchronized O getNextSelectedElement(O element)
	{
		if (currentSelector == null)
			return getNextElement(element);
		O result = null;
		int i = selected.indexOf(element);
		if (i != -1 && selected != null) {
			i++;
			if (i < selected.size())
				result = selected.get(i);
		}
		return result;
	}

	/**
	 * Retrieves the previous element in the sorted array.
	 * 
	 * @param element
	 * @return The previous element
	 * @since 0.9.63
	 */
	public synchronized O getPreviousSelectedElement(O element)
	{
		if (currentSelector == null)
			return getPreviousElement(element);
		O result = null;
		int i = selected.indexOf(element);
		if (i != -1 && selected != null) {
			i--;
			if (i >= 0 && i < selected.size())
				result = selected.get(i);
		}
		return result;
	}

	/**
	 * Retrieves element at index i.
	 * 
	 * @param i
	 * @return Element at index i
	 */
	private O getElement( int i )
	{
		O result = null;
		
		if( elements != null ) {
			result = elements[i];
		}
		return result;
	}
	
	/**
	 * Returns true, if folder shows points to the last page.
	 */
	public synchronized boolean isLastPage()
	{
		return currentPage == pages;
	}
	
	/**
	 * Returns true, if folder shows points to the first page.
	 */
	public synchronized boolean isFirstPage()
	{
		return currentPage == 1;
	}

	/**
	 * Returns true, if elements.equals( lastElementOfTheSortedArray ).
	 * 
	 * @param element
	 */
	public synchronized boolean isLastElement( O element )
	{
		if( elements == null )
			return false;
		return elements[elements.length - 1].equals( element );
	}
	
	/**
	 * Returns true, if elements.equals( firstElementOfTheSortedArray ).
	 * 
	 * @param element
	 */
	public synchronized boolean isFirstElement( O element )
	{
		if( elements == null )
			return false;
		return elements[0].equals( element );
	}
	
	/**
	 * Returns the page this element is on, using the current sort, or 1 if not found
	 * 
	 * @param element
	 * @since 0.9.33
	 */
	public synchronized int getPageOf(O element)
	{
		if (pages <= 1)
			return 1;
		if (elements == null)
			return 1;
		int i = getIndexOf(element);
		if (i < 0)
			return 1;
		return 1 + (i / getPageSize());
	}
}
