/**
 * The contents of this file are subject to the license and copyright
 * detailed in the LICENSE and NOTICE files at the root of the source
 * tree and available online at
 *
 * http://www.dspace.org/license/
 */
package org.dspace.browse;

import java.sql.SQLException;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import org.dspace.content.Collection;
import org.dspace.content.Community;
import org.dspace.content.DCValue;
import org.dspace.content.DSpaceObject;
import org.dspace.content.Item;
import org.dspace.core.Context;
import org.dspace.sort.SortOption;

/**
 * The results of a Browse, including all the contextual information about
 * the query, as well as the results and associated information to create
 * pageable navigation.
 *
 * @author Richard Jones
 */
public class BrowseInfo
{
    /**
     * The results of the browse.
     * FIXME: Unable to generify due to mixed usage
     */
    private List results;

    /**
     * The position of the first element of results within the Browse index.
     * Positions begin with 0.
     */
    private int overallPosition;

    /**
     * The position of the requested object within the results. Offsets begin
     * with 0.
     */
    private int offset;

    /**
     * The total number of items in the browse index.
     */
    private int total;

    /**
     * True if this browse was cached.
     */
    private boolean cached;

    /** the browse index to which this pertains */
    private BrowseIndex browseIndex;

    /** the sort option being used */
    private SortOption sortOption;

    /** is the browse ascending or descending */
    private boolean ascending;

    /** what level of browse are we in?  full and single front pages are 0, single value browse is 1 */
    private int level = 0;

    /** the value browsed upon */
    private String value;

    /** the authority key browsed upon */
    private String authority;

    /** is this a "starts_with" browse? */
    private boolean startsWith = false;

    /** Collection we are constrained to */
    private Collection collection;

    /** Community we are constrained to */
	private Community community;

	/** offset of the item at the top of the next page */
	private int nextOffset = -1;

	/** offset of the item at the top of the previous page */
	private int prevOffset = -1;

	/** the value upon which we are focusing */
	private String focus;

	/** number of results to display per page */
	private int resultsPerPage = -1;

	/** database id of the item upon which we are focusing */
	private int focusItem = -1;

	/** number of metadata elements to display before truncating using "et al" */
	private int etAl = -1;

    /**
     * Constructor
     * FIXME: Unable to generify due to mixed usage
     *
     * @param results
     *            A List of Browse results
     * @param overallPosition
     *            The position of the first returned item in the overall index
     * @param total
     *            The total number of items in the index
     * @param offset
     *            The position of the requested item in the set of results
     */
    public BrowseInfo(List results, int overallPosition, int total, int offset)
    {
        if (results == null)
        {
            throw new IllegalArgumentException("Null result list not allowed");
        }

        this.results = Collections.unmodifiableList(results);
        this.overallPosition = overallPosition;
        this.total = total;
        this.offset = offset;
    }

    /**
     * @return	the number of metadata fields at which to truncate with "et al"
     */
    public int getEtAl()
    {
    	return etAl;
    }

    /**
     * set the number of metadata fields at which to truncate with "et al"
     *
     * @param etAl
     */
    public void setEtAl(int etAl)
    {
    	this.etAl = etAl;
    }

    /**
	 * @return Returns the focusItem.
	 */
	public int getFocusItem()
	{
		return focusItem;
	}

	/**
	 * @param focusItem The focusItem to set.
	 */
	public void setFocusItem(int focusItem)
	{
		this.focusItem = focusItem;
	}

	/**
	 * Does this browse have an item focus (as opposed to one of: no focus,
	 * a value focus)
	 *
	 * @return	true if item focus, false if not
	 */
	public boolean hasItemFocus()
	{
		if (focusItem == -1)
		{
			return false;
		}
		return true;
	}

	/**
	 * @return Returns the resultsPerPage.
	 */
	public int getResultsPerPage()
	{
		return resultsPerPage;
	}

	/**
	 * @param resultsPerPage The resultsPerPage to set.
	 */
	public void setResultsPerPage(int resultsPerPage)
	{
		this.resultsPerPage = resultsPerPage;
	}

	/**
	 * Is there a value associated with this browse
	 *
	 * @return	true if a value, false if not
	 */
	public boolean hasValue()
    {
    	if (this.value != null)
    	{
    		return true;
    	}
    	return false;
    }

	/**
	 * Is there an authority key associated with this browse
	 *
	 * @return	true if  an authority key, false if not
	 */
	public boolean hasAuthority()
    {
    	if (this.authority != null)
    	{
    		return true;
    	}
    	return false;
    }

	/**
	 * Are there results for this browse, or was the result set empty?
	 *
	 * @return	true if results, false if not
	 */
    public boolean hasResults()
    {
    	if (results.size() > 0)
    	{
    		return true;
    	}
    	return false;
    }

    /**
     * @param focus		the value to focus the browse around
     */
    public void setFocus(String focus)
    {
    	this.focus = focus;
    }

    /**
     * @return		the value to focus the browse around
     */
    public String getFocus()
    {
    	return this.focus;
    }

    /**
     * Set the DSpaceObject that is the container for this browse.  If this
     * is not of type Collection or Community, this method will throw an
     * exception
     *
     * @param dso		the container object; a Community or Collection
     * @throws BrowseException
     */
    public void setBrowseContainer(DSpaceObject dso)
    	throws BrowseException
    {
    	if (dso instanceof Collection)
    	{
    		this.collection = (Collection) dso;
    	}
    	else if (dso instanceof Community)
    	{
    		this.community = (Community) dso;
    	}
    	else
    	{
    		throw new BrowseException("The container must be a community or a collection");
    	}
    }

    /**
     * Obtain a DSpaceObject that represents the container object.  This will be
     * a Community or a Collection
     *
     * @return	A DSpaceObject representing a Community or a Collection
     */
    public DSpaceObject getBrowseContainer()
    {
    	if (this.collection != null)
    	{
    		return this.collection;
    	}
    	if (this.community != null)
    	{
    		return this.community;
    	}
    	return null;
    }

    /**
     * @param level		the browse level
     */
    public void setBrowseLevel(int level)
    {
    	this.level = level;
    }

    /**
     * @return	the browse level
     */
    public int getBrowseLevel()
    {
    	return this.level;
    }

    /**
     * @param offset	the database id of the item at the top of the next page
     */
    public void setNextOffset(int offset)
    {
    	this.nextOffset = offset;
    }

    /**
     * @return		the database id of the item at the top of the next page
     */
    public int getNextOffset()
    {
    	return this.nextOffset;
    }

   /**
	 * @return Returns the ascending.
	 */
	public boolean isAscending()
	{
		return ascending;
	}

	/**
	 * @param ascending The ascending to set.
	 */
	public void setAscending(boolean ascending)
	{
		this.ascending = ascending;
	}

	/**
	 * @return Returns the browseIndex.
	 */
	public BrowseIndex getBrowseIndex()
	{
		return browseIndex;
	}

	/**
	 * @param browseIndex The browseIndex to set.
	 */
	public void setBrowseIndex(BrowseIndex browseIndex)
	{
		this.browseIndex = browseIndex;
	}

	/**
	 * @return Returns the prevItem.
	 */
	public int getPrevOffset()
	{
		return prevOffset > -1 ? prevOffset : 0;
	}

	/**
	 * @param prevOffset The prevOffset to set.
	 */
	public void setPrevOffset(int prevOffset)
	{
		this.prevOffset = prevOffset;
	}

	/**
	 * @return Returns the sortOption.
	 */
	public SortOption getSortOption()
	{
		return sortOption;
	}

	/**
	 * @param sortOption The sortOption to set.
	 */
	public void setSortOption(SortOption sortOption)
	{
		this.sortOption = sortOption;
	}

	/**
	 * @return Returns the startsWith.
	 */
	public boolean isStartsWith()
	{
		return startsWith;
	}

	/**
	 * @param startsWith The startsWith to set.
	 */
	public void setStartsWith(boolean startsWith)
	{
		this.startsWith = startsWith;
	}

	/**
	 * @return Returns the value.
	 */
	public String getValue()
	{
		return value;
	}

	/**
	 * @param value The value to set.
	 */
	public void setValue(String value)
	{
		this.value = value;
	}

	/**
	 * @return Returns the authority key.
	 */
	public String getAuthority()
	{
		return authority;
	}

	/**
	 * @param authority The authority key to set.
	 */
	public void setAuthority(String authority)
	{
		this.authority = authority;
	}

	/**
	 * is this a top level (0) browse?  Examples of this are a full item
	 * browse or a single browse.  Other browse types are considered
	 * second level (1)
	 *
	 * @return	true if top level, false if not
	 */
	public boolean isTopLevel()
	{
		if (this.level == 0)
		{
			return true;
		}
		return false;
	}

	/**
	 * Is this a second level (1) browse?  Examples of this are a single
	 * value browse (e.g. all items by a given author)
	 *
	 * @return	true if second level, false if not
	 */
	public boolean isSecondLevel()
	{
		if (this.level == 1)
		{
			return true;
		}
		return false;
	}

    /**
     * The results of the Browse. Each member of the list is either a String array
     * (for the authors browse: first element the value, second element the authority key)
     * or an {@link org.dspace.content.Item}(for the
     * other browses).
     *
     * @return Result list. This list cannot be modified.
     */
    public List getResults()
    {
        return results;
    }

    /**
     * Return the results of the Browse as an array of String array.
     * The first element (i.e. index 0) is the value, the second is the authority key
     *
     * @return The results of the Browse as a String array.
     */
    public String[][] getStringResults()
    {
        return (String[][]) results.toArray(new String[results.size()][2]);
    }

    /**
     * @deprecated
     * @return an empty array of Item.
     */
    public Item[] getItemResults()
    {
    	return new Item[0];
    }

    /**
     * Return the results of the Browse as an Item array.
     *
     * @return The results of the Browse as an Item array.
     */
    public Item[] getItemResults(Context context)
    	throws BrowseException
    {
    	try
    	{
    		BrowseItem[] bis = getBrowseItemResults();
    		Item[] items = new Item[bis.length];
    		for (int i = 0; i < bis.length; i++)
    		{
    			items[i] = Item.find(context, bis[i].getID());
    		}
    		return items;
    	}
    	catch (SQLException e)
    	{
    		throw new BrowseException(e);
    	}
    }

    /**
     * Return the results of the Browse as a BrowseItem array
     *
     * @return		the results of the browse as a BrowseItem array
     */
    public BrowseItem[] getBrowseItemResults()
    {
        return (BrowseItem[]) results.toArray(new BrowseItem[results.size()]);
    }

    /**
     * Return the number of results.
     *
     * @return The number of results.
     */
    public int getResultCount()
    {
        return results.size();
    }

    /**
     * Return the position of the results in index being browsed. This is 0 for
     * the start of the index.
     *
     * @return The position of the results in index being browsed.
     */
    public int getOverallPosition()
    {
        return overallPosition;
    }

    /**
     * Return the total number of items in the index.
     *
     * @return The total number of items in the index.
     */
    public int getTotal()
    {
        return total;
    }

    /**
     * Return the position of the requested item or value in the set of results.
     *
     * @return The position of the requested item or value in the set of results
     */
    public int getOffset()
    {
        return offset;
    }

    /**
     * True if there are no previous results from the browse.
     *
     * @return True if there are no previous results from the browse
     */
    public boolean isFirst()
    {
        return overallPosition == 0;
    }

    /**
     * True if these are the last results from the browse.
     *
     * @return True if these are the last results from the browse
     */
    public boolean isLast()
    {
        return (overallPosition + getResultCount()) == total;
    }

    /**
     * True if this browse was cached.
     */
    public boolean wasCached()
    {
        return cached;
    }

    /**
     * Set whether this browse was cached.
     */
    void setCached(boolean cached)
    {
        this.cached = cached;
    }

    /**
     * are we browsing within a Community container?
     *
     * @return	true if in community, false if not
     */
    public boolean inCommunity()
	{
		if (this.community != null)
		{
			return true;
		}
		return false;
	}

    /**
     * are we browsing within a Collection container
     *
     * @return	true if in collection, false if not
     */
	public boolean inCollection()
	{
		if (this.collection != null)
		{
			return true;
		}
		return false;
	}

	/**
	 * Are there further results for the browse that haven't been returned yet?
	 *
	 * @return	true if next page, false if not
	 */
	public boolean hasNextPage()
	{
		if (nextOffset > -1)
		{
			return true;
		}
		return false;
	}

	/**
	 * Are there results prior to these that haven't been returned here?
	 *
	 * @return	true if previous page, false if not
	 */
	public boolean hasPrevPage()
	{
		if (offset > 0)
		{
			return true;
		}
		return false;
	}

	/**
	 * Does this browse have a focus?
	 *
	 * @return	true if focus, false if not
	 */
	public boolean hasFocus()
	{
		if ("".equals(focus) || focus == null)
		{
			return false;
		}
		return true;
	}

	/**
	 * Get an integer representing the number within the total set of results which
	 * marks the position of the first result in the current sub-set
	 *
	 * @return	the start point of the browse page
	 */
	public int getStart()
	{
		return overallPosition + 1;
	}

	/**
	 * Get an integer representing the number within the total set of results which
	 * marks the position of the last result in the current sub-set
	 *
	 * @return	the end point of the browse page
	 */
	public int getFinish()
	{
		return overallPosition + results.size();
	}

	/**
	 * Utility method for obtaining a string representation of the browse.  This is
	 * useful only for debug
	 */
    public String toString()
    {
    	try
    	{
    		StringBuffer sb = new StringBuffer();

    		// calculate the range for display
    		String from = Integer.toString(overallPosition + 1);
    		String to = Integer.toString(overallPosition + results.size());
    		String of = Integer.toString(total);

    		// report on the positional information of the browse
    		sb.append("BrowseInfo String Representation: ");
    		sb.append("Browsing " + from + " to " + to + " of " + of + " ");

    		// insert the information about which index
    		sb.append("in index: " + browseIndex.getName() +
    				" (data type: " + browseIndex.getDataType() +
    				", display type: " + browseIndex.getDisplayType() + ") ");

    		sb.append("||");

    		// report on the browse scope container
    		String container = "all of DSpace";
    		DSpaceObject theContainer = null;
    		if (inCollection())
    		{
    			container = "collection";
    			theContainer = this.collection;
    		}
    		else if (inCommunity())
    		{
    			container = "community";
    			theContainer = this.community;
    		}

    		String containerID = "no id available/necessary";
    		if (theContainer != null)
    		{
    			containerID = Integer.toString(theContainer.getID()) + " (" + theContainer.getHandle() + ")";
    		}

    		sb.append("Browsing in " + container + ": " + containerID);
    		sb.append("||");

    		// load the item list display configuration
    		ItemListConfig config = new ItemListConfig();

    		// some information about the columns to be displayed
    		if (browseIndex.isItemIndex())
    		{
    			sb.append("Listing over " + Integer.toString(config.numCols()) + " columns: ");
    			for (int k = 1; k <= config.numCols(); k++)
    			{
    				if (k > 1)
    				{
    					sb.append(",");
    				}
    				String[] meta = config.getMetadata(k);
    				sb.append(meta[0] + "." + meta[1] + "." + meta[2]);
    			}

    			if (value != null)
    			{
    				sb.append(" on value: ").append(value);
    			}

    			if (isStartsWith())
    			{
    				sb.append(" sort column starting with: ").append(focus);
    			}
    			else if (hasFocus())
    			{
    				sb.append(" sort column focus: ").append(focus);
    			}
    		}
    		else if (browseIndex.isMetadataIndex())
    		{
    			sb.append("Listing single column: ").append(browseIndex.getMetadata());
    			if (isStartsWith())
    			{
    				sb.append(" sort column starting with: ").append(focus);
    			}
    			else if (hasFocus())
    			{
    				sb.append(" sort column focus: ").append(focus);
    			}
    		}

    		sb.append("||");

    		// some information about how the data is sorted
    		String direction = (ascending ? "ASC" : "DESC");
    		sb.append("Sorting by: " + sortOption.getMetadata() + " " + direction +
    				" (option " + Integer.toString(sortOption.getNumber()) + ")");
    		sb.append("||");

    		// output the results
    		if (browseIndex.isMetadataIndex() && !isSecondLevel())
    		{
    			sb.append(valueListingString());
    		}
    		else if (browseIndex.isItemIndex() || isSecondLevel())
    		{
    			sb.append(fullListingString(config));
    		}

    		sb.append("||");

    		// tell us what the next and previous values are going to be
    		sb.append("Top of next page: ");
    		if (hasNextPage())
    		{
				sb.append("offset: ").append(Integer.toString(this.nextOffset));
    		}
    		else
    		{
    			sb.append("n/a");
    		}
    		sb.append(";");

    		sb.append("Top of previous page: ");
    		if (hasPrevPage())
    		{
				sb.append("offset: ").append(Integer.toString(this.prevOffset));
    		}
    		else
    		{
    			sb.append("n/a");
    		}

    		sb.append("||");

    		return sb.toString();
    	}
    	catch (SQLException e)
    	{
    		return e.getMessage();
    	}
    	catch (BrowseException e)
    	{
    		return e.getMessage();
    	}
    }

    /**
     * A utility method for generating a string to represent a single item's
     * entry in the browse
     *
     * @param config
     * @return
     * @throws SQLException
     */
    private String fullListingString(ItemListConfig config)
    	throws SQLException
    {
    	// report on all the results contained herein
    	StringBuffer sb = new StringBuffer();

		Iterator itr = results.iterator();
		while (itr.hasNext())
		{
			BrowseItem bi = (BrowseItem) itr.next();
			if (bi == null)
			{
				sb.append("{{ NULL ITEM }}");
				break;
			}
			sb.append("{{Item ID: " + Integer.toString(bi.getID()) + " :: ");

			for (int j = 1; j <= config.numCols(); j++)
			{
				String[] md = config.getMetadata(j);
				if (md == null)
    			{
    				sb.append("{{ NULL METADATA }}");
    				break;
    			}
				DCValue[] values = bi.getMetadata(md[0], md[1], md[2], Item.ANY);
				StringBuffer value = new StringBuffer();
				if (values != null)
				{
					for (int i = 0; i < values.length; i++)
					{
						if (i > 0)
						{
							value.append(",");
						}
						value.append(values[i].value);
					}
				}
				else
				{
					value.append("-");
				}
				String metadata = "[" + md[0] + "." + md[1] + "." + md[2] + ":" + value.toString() + "]";
				sb.append(metadata);
			}

			sb.append("}}");
		}

		return sb.toString();
    }

    /**
     * A utility method for representing a single value in the browse
     *
     * @return
     */
    private String valueListingString()
    {
    	// report on all the results contained herein
    	StringBuffer sb = new StringBuffer();

		Iterator itr = results.iterator();
		while (itr.hasNext())
		{
			String theValue = (String) itr.next();
			if (theValue == null)
			{
				sb.append("{{ NULL VALUE }}");
				break;
			}
			sb.append("{{Value: " + theValue + "}}");
		}

		return sb.toString();
    }
}
