/**
 * 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.io.IOException;
import java.util.ArrayList;
import java.util.StringTokenizer;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.dspace.core.ConfigurationManager;
import org.dspace.sort.SortOption;
import org.dspace.sort.SortException;

/**
 * This class holds all the information about a specifically configured 
 * BrowseIndex.  It is responsible for parsing the configuration, understanding
 * about what sort options are available, and what the names of the database
 * tables that hold all the information are actually called.
 * 
 * @author Richard Jones
 */
public final class BrowseIndex
{
	/** the configuration number, as specified in the config */
    /** used for single metadata browse tables for generating the table name */
    private int number;
    
    /** the name of the browse index, as specified in the config */
    private String name;

    /** the SortOption for this index (only valid for item indexes) */
    private SortOption sortOption;

    /** the value of the metadata, as specified in the config */
    private String metadataAll;

    /** the metadata fields, as an array */
    private String[] metadata;

    /** the datatype of the index, as specified in the config */
    private String datatype;
    
    /** the display type of the metadata, as specified in the config */
    private String displayType;
    
    /** base name for tables, sequences */
    private String tableBaseName;
    
    /** a three part array of the metadata bits (e.g. dc.contributor.author) */
    private String[][] mdBits;

    /** default order (asc / desc) for this index */
    private String defaultOrder = SortOption.ASCENDING;

    /** additional 'internal' tables that are always defined */
    private static BrowseIndex itemIndex      = new BrowseIndex("bi_item");
    private static BrowseIndex withdrawnIndex = new BrowseIndex("bi_withdrawn");

    /**
     * Ensure no one else can create these
     */
    private BrowseIndex()
    {
    }
    
    /**
     * Constructor for creating generic / internal index objects
     * @param baseName The base of the table name
     */
    private BrowseIndex(String baseName)
    {
        try
        {
            number = -1;
            tableBaseName = baseName;
            displayType = "item";
            sortOption = SortOption.getDefaultSortOption();
        }
        catch (SortException se)
        {
            // FIXME Exception handling
        }
    }
    
    /**
     * Create a new BrowseIndex object using the definition from the configuration,
     * and the number of the configuration option.  The definition should be of
     * the form:
     * 
     * <code>
     * [name]:[metadata]:[data type]:[display type]
     * </code>
     * 
     * [name] is a freetext name for the field
     * [metadata] is the usual format of the metadata such as dc.contributor.author
     * [data type] must be either "title", "date" or "text"
     * [display type] must be either "single" or "full"
     * 
     * @param definition	the configuration definition of this index
     * @param number		the configuration number of this index
     * @throws BrowseException 
     */
    private BrowseIndex(String definition, int number)
    	throws BrowseException
    {
        try
        {
            boolean valid = true;
            this.defaultOrder = SortOption.ASCENDING;
            this.number = number;

            String rx = "(\\w+):(\\w+):([\\w\\.\\*,]+):?(\\w*):?(\\w*)";
            Pattern pattern = Pattern.compile(rx);
            Matcher matcher = pattern.matcher(definition);

            if (matcher.matches())
            {
                name = matcher.group(1);
                displayType = matcher.group(2);

                if (isMetadataIndex())
                {
                    metadataAll = matcher.group(3);
                    datatype = matcher.group(4);

                    if (metadataAll != null)
                    {
                        metadata = metadataAll.split(",");
                    }

                    if (metadata == null || metadata.length == 0)
                    {
                        valid = false;
                    }

                    if (datatype == null || datatype.equals(""))
                    {
                        valid = false;
                    }

                    // If an optional ordering configuration is supplied,
                    // set the defaultOrder appropriately (asc or desc)
                    if (matcher.groupCount() > 4)
                    {
                        String order = matcher.group(5);
                        if (SortOption.DESCENDING.equalsIgnoreCase(order))
                        {
                            this.defaultOrder = SortOption.DESCENDING;
                        }
                    }

                    tableBaseName = getItemBrowseIndex().tableBaseName;
                }
                else if (isItemIndex())
                {
                    String sortName = matcher.group(3);

                    for (SortOption so : SortOption.getSortOptions())
                    {
                        if (so.getName().equals(sortName))
                        {
                            sortOption = so;
                        }
                    }

                    if (sortOption == null)
                    {
                        valid = false;
                    }

                    // If an optional ordering configuration is supplied,
                    // set the defaultOrder appropriately (asc or desc)
                    if (matcher.groupCount() > 3)
                    {
                        String order = matcher.group(4);
                        if (SortOption.DESCENDING.equalsIgnoreCase(order))
                        {
                            this.defaultOrder = SortOption.DESCENDING;
                        }
                    }

                    tableBaseName = getItemBrowseIndex().tableBaseName;
                }
                else
                {
                    valid = false;
                }
            }
            else
            {
                valid = false;
            }

            if (!valid)
            {
                throw new BrowseException("Browse Index configuration is not valid: webui.browse.index." +
                        number + " = " + definition);
            }
        }
        catch (SortException se)
        {
            throw new BrowseException("Error in SortOptions", se);
        }
    }

    /**
     * @return Default order for this index, null if not specified
     */
    public String getDefaultOrder()
    {
        return defaultOrder;
    }

    /**
	 * @return Returns the datatype.
	 */
	public String getDataType()
	{
        if (sortOption != null)
        {
            return sortOption.getType();
        }

		return datatype;
	}

	/**
	 * @return Returns the displayType.
	 */
	public String getDisplayType()
	{
        return displayType;
	}

    /**
     * @return Returns the number of metadata fields for this index
     */
    public int getMetadataCount()
    {
        if (isMetadataIndex())
        {
            return metadata.length;
        }

        return 0;
    }

    /**
	 * @return Returns the mdBits.
	 */
	public String[] getMdBits(int idx)
	{
	    if (isMetadataIndex())
        {
            return mdBits[idx];
        }
	    
	    return null;
	}

	/**
	 * @return Returns the metadata.
	 */
	public String getMetadata()
	{
        return metadataAll;
	}

    public String getMetadata(int idx)
    {
        return metadata[idx];
    }

	/**
	 * @return Returns the name.
	 */
	public String getName()
	{
		return name;
	}

	/**
	 * @param name The name to set.
	 */
//	public void setName(String name)
//	{
//		this.name = name;
//	}
	
	/**
	 * Get the SortOption associated with this index.
	 */
	public SortOption getSortOption()
	{
	    return sortOption;
	}
	
	/**
	 * Populate the internal array containing the bits of metadata, for
	 * ease of use later
	 */
	public void generateMdBits()
    {
    	try
    	{
    	    if (isMetadataIndex())
            {
                mdBits = new String[metadata.length][];
                for (int i = 0; i < metadata.length; i++)
                {
                    mdBits[i] = interpretField(metadata[i], null);
                }
            }
        }
    	catch(IOException e)
    	{
    		// it's not obvious what we really ought to do here
    		//log.error("caught exception: ", e);
    	}
    }
    
	/**
	 * Get the name of the sequence that will be used in the given circumstances
	 * 
	 * @param isDistinct	is a distinct table
	 * @param isMap			is a map table
	 * @return				the name of the sequence
	 */
    public String getSequenceName(boolean isDistinct, boolean isMap)
    {
        if (isDistinct || isMap)
        {
            return BrowseIndex.getSequenceName(number, isDistinct, isMap);
        }
        
        return BrowseIndex.getSequenceName(tableBaseName, isDistinct, isMap);
    }
    
    /**
     * Get the name of the sequence that will be used in the given circumstances
     * 
     * @param number		the index configuration number
     * @param isDistinct	is a distinct table
     * @param isMap			is a map table
     * @return				the name of the sequence
     */
    public static String getSequenceName(int number, boolean isDistinct, boolean isMap)
    {
        return BrowseIndex.getSequenceName(makeTableBaseName(number), isDistinct, isMap);
    }
    
    /**
     * Generate a sequence name from the given base
     * @param baseName
     * @param isDistinct
     * @param isMap
     * @return
     */
    private static String getSequenceName(String baseName, boolean isDistinct, boolean isMap)
    {
        if (isDistinct)
        {
            baseName = baseName + "_dis";
        }
        else if (isMap)
        {
            baseName = baseName + "_dmap";
        }
        
        baseName = baseName + "_seq";
        
        return baseName;
    }
    
    /**
     * Get the name of the table for the given set of circumstances
     * This is provided solely for cleaning the database, where you are
     * trying to create table names that may not be reflected in the current index
     * 
     * @param number		the index configuration number
     * @param isCommunity	whether this is a community constrained index (view)
     * @param isCollection	whether this is a collection constrained index (view)
     * @param isDistinct	whether this is a distinct table
     * @param isMap			whether this is a distinct map table
     * @return				the name of the table
     * @deprecated 1.5
     */
    public static String getTableName(int number, boolean isCommunity, boolean isCollection, boolean isDistinct, boolean isMap)
    {
        return BrowseIndex.getTableName(makeTableBaseName(number), isCommunity, isCollection, isDistinct, isMap);
    }
    
    /**
     * Generate a table name from the given base
     * @param baseName
     * @param isCommunity
     * @param isCollection
     * @param isDistinct
     * @param isMap
     * @return
     */
    private static String getTableName(String baseName, boolean isCommunity, boolean isCollection, boolean isDistinct, boolean isMap)
    {
    	// isDistinct is meaningless in relation to isCommunity and isCollection
    	// so we bounce that back first, ignoring other arguments
    	if (isDistinct)
    	{
    		return baseName + "_dis";
    	}
    	
    	// isCommunity and isCollection are mutually exclusive
    	if (isCommunity)
    	{
    		baseName = baseName + "_com"; 
    	}
    	else if (isCollection)
    	{
    		baseName = baseName + "_col";
    	}
    	
    	// isMap is additive to isCommunity and isCollection
    	if (isMap)
    	{
    		baseName = baseName + "_dmap";
    	}
    	
    	return baseName;
    }
    
    /**
     * Get the name of the table in the given circumstances
     * 
     * @param isCommunity	whether this is a community constrained index (view)
     * @param isCollection	whether this is a collection constrained index (view)
     * @param isDistinct	whether this is a distinct table
     * @param isMap			whether this is a distinct map table
     * @return				the name of the table
     * @deprecated 1.5
     */
    public String getTableName(boolean isCommunity, boolean isCollection, boolean isDistinct, boolean isMap)
    {
        if (isDistinct || isMap)
        {
            return BrowseIndex.getTableName(number, isCommunity, isCollection, isDistinct, isMap);
        }
        
        return BrowseIndex.getTableName(tableBaseName, isCommunity, isCollection, isDistinct, isMap);
    }
    
    /**
     * Get the name of the table in the given circumstances.  This is the same as calling
     * 
     * <code>
     * getTableName(isCommunity, isCollection, false, false);
     * </code>
     * 
     * @param isCommunity	whether this is a community constrained index (view)
     * @param isCollection	whether this is a collection constrained index (view)
     * @return				the name of the table
     * @deprecated 1.5
     */
    public String getTableName(boolean isCommunity, boolean isCollection)
    {
        return getTableName(isCommunity, isCollection, false, false);
    }
    
    /**
     * Get the default index table name.  This is the same as calling
     * 
     * <code>
     * getTableName(false, false, false, false);
     * </code>
     * 
     * @return
     */
    public String getTableName()
    {
        return getTableName(false, false, false, false);
    }
    
    /**
     * Get the table name for the given set of circumstances
     * 
     * This is the same as calling:
     * 
     * <code>
     * getTableName(isCommunity, isCollection, isDistinct, false);
     * </code>
     * 
     * @param isDistinct	is this a distinct table
     * @param isCommunity
     * @param isCollection
     * @return
     * @deprecated 1.5
     */
    public String getTableName(boolean isDistinct, boolean isCommunity, boolean isCollection)
    {
    	return getTableName(isCommunity, isCollection, isDistinct, false);
    }
    
    /**
     * Get the default name of the distinct map table.  This is the same as calling
     * 
     * <code>
     * getTableName(false, false, false, true);
     * </code>
     * 
     * @return
     */
    public String getMapTableName()
    {
    	return getTableName(false, false, false, true);
    }
    
    /**
     * Get the default name of the distinct table.  This is the same as calling
     *
     * <code>
     * getTableName(false, false, true, false);
     * </code>
     *
     * @return
     */
    public String getDistinctTableName()
    {
    	return getTableName(false, false, true, false);
    }

    /**
     * Get the name of the column that is used to store the default value column
     * 
     * @return	the name of the value column
     */
    public String getValueColumn()
    {
        if (!isDate())
        {
            return "sort_text_value";
        }
        else
        {
            return "text_value";
        }
    }
    
    /**
     * Get the name of the primary key index column
     * 
     * @return	the name of the primary key index column
     */
    public String getIndexColumn()
    {
        return "id";
    }
    
    /**
     * Is this browse index type for a title?
     * 
     * @return	true if title type, false if not
     */
//    public boolean isTitle()
//    {
//        return "title".equals(getDataType());
//    }
    
    /**
     * Is the browse index type for a date?
     * 
     * @return	true if date type, false if not
     */
    public boolean isDate()
    {
        return "date".equals(getDataType());
    }
    
    /**
     * Is the browse index type for a plain text type?
     * 
     * @return	true if plain text type, false if not
     */
//    public boolean isText()
//    {
//        return "text".equals(getDataType());
//    }
    
    /**
     * Is the browse index of display type single?
     * 
     * @return true if singe, false if not
     */
    public boolean isMetadataIndex()
    {
        return displayType  != null && displayType.startsWith("metadata");
    }
    
    /**
     * Is the browse index authority value?
     *
     * @return true if authority, false if not
     */
    public boolean isAuthorityIndex()
    {
        return "metadataAuthority".equals(displayType);
    }
    
    /**
     * Is the browse index of display type full?
     * 
     * @return	true if full, false if not
     */
    public boolean isItemIndex()
    {
        return "item".equals(displayType);
    }
    
    /**
     * Get the field for sorting associated with this index
     * @return
     * @throws BrowseException
     */
    public String getSortField(boolean isSecondLevel) throws BrowseException
    {
        String focusField;
        if (isMetadataIndex() && !isSecondLevel)
        {
            focusField = "sort_value";
        }
        else
        {
            if (sortOption != null)
            {
                focusField = "sort_" + sortOption.getNumber();
            }
            else
            {
                focusField = "sort_1";  // Use the first sort column
            }
        }
        
        return focusField;
    }
    
    /**
     * @deprecated
     * @return
     * @throws BrowseException
     */
    public static String[] tables()
            throws BrowseException
    {
        BrowseIndex[] bis = getBrowseIndices();
        String[] returnTables = new String[bis.length];
        for (int i = 0; i < bis.length; i++)
        {
            returnTables[i] = bis[i].getTableName();
        }

        return returnTables;
    }
    
    /**
     * Get an array of all the browse indices for the current configuration
     * 
     * @return	an array of all the current browse indices
     * @throws BrowseException
     */
    public static BrowseIndex[] getBrowseIndices()
    	throws BrowseException
    {
        int idx = 1;
        String definition;
        ArrayList<BrowseIndex> browseIndices = new ArrayList<BrowseIndex>();

        while ( ((definition = ConfigurationManager.getProperty("webui.browse.index." + idx))) != null)
        {
            BrowseIndex bi = new BrowseIndex(definition, idx);
            browseIndices.add(bi);
            idx++;
        }

        BrowseIndex[] bis = new BrowseIndex[browseIndices.size()];
        bis = browseIndices.toArray(bis);

        return bis;
    }

    /**
     * Get the browse index from configuration with the specified name.
     * The name is the first part of the browse configuration
     *
     * @param name		the name to retrieve
     * @return			the specified browse index
     * @throws BrowseException
     */
    public static BrowseIndex getBrowseIndex(String name)
    	throws BrowseException
    {
        for (BrowseIndex bix : BrowseIndex.getBrowseIndices())
        {
            if (bix.getName().equals(name))
            {
                return bix;
            }
        }
         
        return null;
    }
    
    /**
     * Get the configured browse index that is defined to use this sort option
     * 
     * @param so
     * @return
     * @throws BrowseException
     */
    public static BrowseIndex getBrowseIndex(SortOption so) throws BrowseException
    {
        for (BrowseIndex bix : BrowseIndex.getBrowseIndices())
        {
            if (bix.getSortOption() == so)
            {
                return bix;
            }
        }
        
        return null;
    }
    
    /**
     * Get the internally defined browse index for archived items
     * 
     * @return
     */
    public static BrowseIndex getItemBrowseIndex()
    {
        return BrowseIndex.itemIndex;
    }
    
    /**
     * Get the internally defined browse index for withdrawn items
     * @return
     */
    public static BrowseIndex getWithdrawnBrowseIndex()
    {
        return BrowseIndex.withdrawnIndex;
    }
    
    /**
     * Take a string representation of a metadata field, and return it as an array.
     * This is just a convenient utility method to basically break the metadata 
     * representation up by its delimiter (.), and stick it in an array, inserting
     * the value of the init parameter when there is no metadata field part.
     * 
     * @param mfield	the string representation of the metadata
     * @param init	the default value of the array elements
     * @return	a three element array with schema, element and qualifier respectively
     */
    public String[] interpretField(String mfield, String init)
    	throws IOException
    {
    	StringTokenizer sta = new StringTokenizer(mfield, ".");
    	String[] field = {init, init, init};
    	
    	int i = 0;
    	while (sta.hasMoreTokens())
    	{
    		field[i++] = sta.nextToken();
    	}
    	
    	// error checks to make sure we have at least a schema and qualifier for both
    	if (field[0] == null || field[1] == null)
    	{
    		throw new IOException("at least a schema and element be " +
    				"specified in configuration.  You supplied: " + mfield);
    	}
    	
    	return field;
    }

    /**
     * Does this browse index represent one of the internal item indexes
     * 
     * @return
     */
    public boolean isInternalIndex()
    {
        return (this == itemIndex || this == withdrawnIndex);
    }

    /**
     * Generate a base table name
     * @param number
     * @return
     */
    private static String makeTableBaseName(int number)
    {
        return "bi_" + Integer.toString(number);
    }
}
