/**
 * 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.content;

import java.io.IOException;
import java.io.InputStream;
import java.sql.SQLException;
import java.util.*;

import org.apache.log4j.Logger;
import org.dspace.authorize.AuthorizeConfiguration;
import org.dspace.authorize.AuthorizeException;
import org.dspace.authorize.AuthorizeManager;
import org.dspace.authorize.ResourcePolicy;
import org.dspace.core.ConfigurationManager;
import org.dspace.core.Constants;
import org.dspace.core.Context;
import org.dspace.core.LogManager;
import org.dspace.event.Event;
import org.dspace.storage.rdbms.DatabaseManager;
import org.dspace.storage.rdbms.TableRow;
import org.dspace.storage.rdbms.TableRowIterator;

/**
 * Class representing bundles of bitstreams stored in the DSpace system
 * <P>
 * The corresponding Bitstream objects are loaded into memory. At present, there
 * is no metadata associated with bundles - they are simple containers. Thus,
 * the <code>update</code> method doesn't do much yet. Creating, adding or
 * removing bitstreams has instant effect in the database.
 * 
 * @author Robert Tansley
 * @version $Revision$
 */
public class Bundle extends DSpaceObject
{
    /** log4j logger */
    private static Logger log = Logger.getLogger(Bundle.class);

    /** Our context */
    private Context ourContext;

    /** The table row corresponding to this bundle */
    private TableRow bundleRow;

    /** The bitstreams in this bundle */
    private List<Bitstream> bitstreams;

    /** Flag set when data is modified, for events */
    private boolean modified;

    /** Flag set when metadata is modified, for events */
    private boolean modifiedMetadata;

    /**
     * Construct a bundle object with the given table row
     * 
     * @param context
     *            the context this object exists in
     * @param row
     *            the corresponding row in the table
     */
    Bundle(Context context, TableRow row) throws SQLException
    {
        ourContext = context;
        bundleRow = row;
        bitstreams = new ArrayList<Bitstream>();
        String bitstreamOrderingField  = ConfigurationManager.getProperty("webui.bitstream.order.field");
        String bitstreamOrderingDirection   = ConfigurationManager.getProperty("webui.bitstream.order.direction");

        if (bitstreamOrderingField == null)
        {
            bitstreamOrderingField = "sequence_id";
        }

        if (bitstreamOrderingDirection == null)
        {
            bitstreamOrderingDirection = "ASC";
        }

        StringBuilder query = new StringBuilder();
        query.append("SELECT bitstream.*,bundle2bitstream.bitstream_order FROM bitstream, bundle2bitstream WHERE");
        query.append(" bundle2bitstream.bitstream_id=bitstream.bitstream_id AND");
        query.append(" bundle2bitstream.bundle_id= ?");
        query.append(" ORDER BY ");
        query.append(bitstreamOrderingField);
        query.append(" ");
        query.append(bitstreamOrderingDirection);

        // Get bitstreams
        TableRowIterator tri = DatabaseManager.query(
                ourContext,
                query.toString(),
                bundleRow.getIntColumn("bundle_id"));

        try
        {
            while (tri.hasNext())
            {
                TableRow r = tri.next();

                // First check the cache
                Bitstream fromCache = (Bitstream) context.fromCache(
                        Bitstream.class, r.getIntColumn("bitstream_id"));

                if (fromCache != null)
                {
                    bitstreams.add(fromCache);
                }
                else
                {
                    //Since bitstreams can be ordered by a column in bundle2bitstream
                    //We cannot use queryTable & so we need to add our table later on
                    r.setTable("bitstream");
                    bitstreams.add(new Bitstream(ourContext, r));
                }
            }
        }
        finally
        {
            // close the TableRowIterator to free up resources
            if (tri != null)
            {
                tri.close();
            }
        }

        // Cache ourselves
        context.cache(this, row.getIntColumn("bundle_id"));

        modified = false;
        modifiedMetadata = false;
    }

    /**
     * Get a bundle from the database. The bundle and bitstream metadata are all
     * loaded into memory.
     * 
     * @param context
     *            DSpace context object
     * @param id
     *            ID of the bundle
     * 
     * @return the bundle, or null if the ID is invalid.
     */
    public static Bundle find(Context context, int id) throws SQLException
    {
        // First check the cache
        Bundle fromCache = (Bundle) context.fromCache(Bundle.class, id);

        if (fromCache != null)
        {
            return fromCache;
        }

        TableRow row = DatabaseManager.find(context, "bundle", id);

        if (row == null)
        {
            if (log.isDebugEnabled())
            {
                log.debug(LogManager.getHeader(context, "find_bundle",
                        "not_found,bundle_id=" + id));
            }

            return null;
        }
        else
        {
            if (log.isDebugEnabled())
            {
                log.debug(LogManager.getHeader(context, "find_bundle",
                        "bundle_id=" + id));
            }

            return new Bundle(context, row);
        }
    }

    /**
     * Create a new bundle, with a new ID. This method is not public, since
     * bundles need to be created within the context of an item. For this
     * reason, authorisation is also not checked; that is the responsibility of
     * the caller.
     * 
     * @param context
     *            DSpace context object
     * 
     * @return the newly created bundle
     */
    static Bundle create(Context context) throws SQLException
    {
        // Create a table row
        TableRow row = DatabaseManager.create(context, "bundle");

        log.info(LogManager.getHeader(context, "create_bundle", "bundle_id="
                + row.getIntColumn("bundle_id")));

        context.addEvent(new Event(Event.CREATE, Constants.BUNDLE, row.getIntColumn("bundle_id"), null));

        return new Bundle(context, row);
    }

    /**
     * Get the internal identifier of this bundle
     * 
     * @return the internal identifier
     */
    public int getID()
    {
        return bundleRow.getIntColumn("bundle_id");
    }

    /**
     * Get the name of the bundle
     * 
     * @return name of the bundle (ORIGINAL, TEXT, THUMBNAIL) or NULL if not set
     */
    public String getName()
    {
        return bundleRow.getStringColumn("name");
    }

    /**
     * Set the name of the bundle
     * 
     * @param name
     *            string name of the bundle (ORIGINAL, TEXT, THUMBNAIL) are the
     *            values currently used
     */
    public void setName(String name)
    {
        bundleRow.setColumn("name", name);
        modifiedMetadata = true;
    }

    /**
     * Get the primary bitstream ID of the bundle
     * 
     * @return primary bitstream ID or -1 if not set
     */
    public int getPrimaryBitstreamID()
    {
        return bundleRow.getIntColumn("primary_bitstream_id");
    }

    /**
     * Set the primary bitstream ID of the bundle
     * 
     * @param bitstreamID
     *            int ID of primary bitstream (e.g. index html file)
     */
    public void setPrimaryBitstreamID(int bitstreamID)
    {
        bundleRow.setColumn("primary_bitstream_id", bitstreamID);
        modified = true;
    }

    /**
     * Unset the primary bitstream ID of the bundle
     */
    public void unsetPrimaryBitstreamID()
    {
    	bundleRow.setColumnNull("primary_bitstream_id");
    }
    
    public String getHandle()
    {
        // No Handles for bundles
        return null;
    }

    /**
     * @param name
     *            name of the bitstream you're looking for
     * 
     * @return the bitstream or null if not found
     */
    public Bitstream getBitstreamByName(String name)
    {
        Bitstream target = null;

        Iterator i = bitstreams.iterator();

        while (i.hasNext())
        {
            Bitstream b = (Bitstream) i.next();

            if (name.equals(b.getName()))
            {
                target = b;

                break;
            }
        }

        return target;
    }

    /**
     * Get the bitstreams in this bundle
     * 
     * @return the bitstreams
     */
    public Bitstream[] getBitstreams()
    {
        Bitstream[] bitstreamArray = new Bitstream[bitstreams.size()];
        bitstreamArray = (Bitstream[]) bitstreams.toArray(bitstreamArray);

        return bitstreamArray;
    }

    /**
     * Get the items this bundle appears in
     * 
     * @return array of <code>Item</code> s this bundle appears in
     */
    public Item[] getItems() throws SQLException
    {
        List<Item> items = new ArrayList<Item>();

        // Get items
        TableRowIterator tri = DatabaseManager.queryTable(
        		ourContext, "item",
                "SELECT item.* FROM item, item2bundle WHERE " +
                "item2bundle.item_id=item.item_id AND " +
                "item2bundle.bundle_id= ? ",
                bundleRow.getIntColumn("bundle_id"));

        try
        {
            while (tri.hasNext())
            {
                TableRow r = (TableRow) tri.next();

                // Used cached copy if there is one
                Item fromCache = (Item) ourContext.fromCache(Item.class, r
                        .getIntColumn("item_id"));

                if (fromCache != null)
                {
                    items.add(fromCache);
                }
                else
                {
                    items.add(new Item(ourContext, r));
                }
            }
        }
        finally
        {
            // close the TableRowIterator to free up resources
            if (tri != null)
            {
                tri.close();
            }
        }

        Item[] itemArray = new Item[items.size()];
        itemArray = (Item[]) items.toArray(itemArray);

        return itemArray;
    }

    /**
     * Create a new bitstream in this bundle.
     * 
     * @param is
     *            the stream to read the new bitstream from
     * 
     * @return the newly created bitstream
     */
    public Bitstream createBitstream(InputStream is) throws AuthorizeException,
            IOException, SQLException
    {
        // Check authorisation
        AuthorizeManager.authorizeAction(ourContext, this, Constants.ADD);

        Bitstream b = Bitstream.create(ourContext, is);

        // FIXME: Set permissions for bitstream
        addBitstream(b);

        return b;
    }

    /**
     * Create a new bitstream in this bundle. This method is for registering
     * bitstreams.
     *
     * @param assetstore corresponds to an assetstore in dspace.cfg
     * @param bitstreamPath the path and filename relative to the assetstore 
     * @return  the newly created bitstream
     * @throws IOException
     * @throws SQLException
     */
    public Bitstream registerBitstream(int assetstore, String bitstreamPath)
        throws AuthorizeException, IOException, SQLException
    {
        // check authorisation
        AuthorizeManager.authorizeAction(ourContext, this, Constants.ADD);

        Bitstream b = Bitstream.register(ourContext, assetstore, bitstreamPath);

        // FIXME: Set permissions for bitstream

        addBitstream(b);
        return b;
    }

    /**
     * Add an existing bitstream to this bundle
     * 
     * @param b
     *            the bitstream to add
     */
    public void addBitstream(Bitstream b) throws SQLException,
            AuthorizeException
    {
        // Check authorisation
        AuthorizeManager.authorizeAction(ourContext, this, Constants.ADD);

        log.info(LogManager.getHeader(ourContext, "add_bitstream", "bundle_id="
                + getID() + ",bitstream_id=" + b.getID()));

        // First check that the bitstream isn't already in the list
        for (int i = 0; i < bitstreams.size(); i++)
        {
            Bitstream existing = (Bitstream) bitstreams.get(i);

            if (b.getID() == existing.getID())
            {
                // Bitstream is already there; no change
                return;
            }
        }

        // Add the bitstream object
        bitstreams.add(b);

        ourContext.addEvent(new Event(Event.ADD, Constants.BUNDLE, getID(), Constants.BITSTREAM, b.getID(), String.valueOf(b.getSequenceID())));

        // copy authorization policies from bundle to bitstream
        // FIXME: multiple inclusion is affected by this...
        AuthorizeManager.inheritPolicies(ourContext, this, b);

        //Determine the current highest bitstream order in our bundle2bitstream table 
        //This will always append a newly added bitstream as the last one 
        int bitstreamOrder = 0;  //bitstream order starts at '0' index
        TableRow tableRow = DatabaseManager.querySingle(ourContext, "SELECT MAX(bitstream_order) as max_value FROM bundle2bitstream WHERE bundle_id=?", getID());
        if(tableRow != null){
            bitstreamOrder = tableRow.getIntColumn("max_value") + 1;
        }

        // Add the mapping row to the database
        TableRow mappingRow = DatabaseManager.row("bundle2bitstream");
        mappingRow.setColumn("bundle_id", getID());
        mappingRow.setColumn("bitstream_id", b.getID());
        mappingRow.setColumn("bitstream_order", bitstreamOrder);
        DatabaseManager.insert(ourContext, mappingRow);
    }

    /**
     * Changes bitstream order according to the array
     * @param bitstreamIds the identifiers in the order they are to be set
     * @throws SQLException when an SQL error has occurred (querying DSpace)
     * @throws AuthorizeException If the user can't make the changes
     */
    public void setOrder(int bitstreamIds[]) throws AuthorizeException, SQLException {
        AuthorizeManager.authorizeAction(ourContext, this, Constants.WRITE);

        //Map the bitstreams of the bundle by identifier
        Map<Integer, Bitstream> bitstreamMap = new HashMap<Integer, Bitstream>();
        for (Bitstream bitstream : bitstreams) {
            bitstreamMap.put(bitstream.getID(), bitstream);
        }

        //We need to also reoder our cached bitstreams list
        bitstreams = new ArrayList<Bitstream>();
        for (int i = 0; i < bitstreamIds.length; i++) {
            int bitstreamId = bitstreamIds[i];

            //TODO: take into account the asc & desc ! from the dspace.cfg
            TableRow row = DatabaseManager.querySingleTable(ourContext, "bundle2bitstream",
                    "SELECT * FROM bundle2bitstream WHERE bitstream_id= ? ", bitstreamId);

            if(row == null){
                //This should never occur but just in case
                log.warn(LogManager.getHeader(ourContext, "Invalid bitstream id while changing bitstream order", "Bundle: " + getID() + ", bitstream id: " + bitstreamId));
            }else{
                row.setColumn("bitstream_order", i);
                DatabaseManager.update(ourContext, row);
            }

            // Place the bitstream in the list of bitstreams in this bundle
            bitstreams.add(bitstreamMap.get(bitstreamId));
        }

        //The order of the bitstreams has changed, ensure that we update the last modified of our item
        Item owningItem = (Item) getParentObject();
        if(owningItem != null)
        {
            owningItem.updateLastModified();
            owningItem.update();

        }
    }

    /**
     * Remove a bitstream from this bundle - the bitstream is only deleted if
     * this was the last reference to it
     * <p>
     * If the bitstream in question is the primary bitstream recorded for the
     * bundle the primary bitstream field is unset in order to free the
     * bitstream from the foreign key constraint so that the
     * <code>cleanup</code> process can run normally.
     * 
     * @param b
     *            the bitstream to remove
     */
    public void removeBitstream(Bitstream b) throws AuthorizeException,
            SQLException, IOException
    {
        // Check authorisation
        AuthorizeManager.authorizeAction(ourContext, this, Constants.REMOVE);

        log.info(LogManager.getHeader(ourContext, "remove_bitstream",
                "bundle_id=" + getID() + ",bitstream_id=" + b.getID()));

        // Remove from internal list of bitstreams
        ListIterator li = bitstreams.listIterator();

        while (li.hasNext())
        {
            Bitstream existing = (Bitstream) li.next();

            if (b.getID() == existing.getID())
            {
                // We've found the bitstream to remove
                li.remove();
            }
        }

        ourContext.addEvent(new Event(Event.REMOVE, Constants.BUNDLE, getID(), Constants.BITSTREAM, b.getID(), String.valueOf(b.getSequenceID())));

        //Ensure that the last modified from the item is triggered !
        Item owningItem = (Item) getParentObject();
        if(owningItem != null)
        {
            owningItem.updateLastModified();
            owningItem.update();

        }

        // In the event that the bitstream to remove is actually
        // the primary bitstream, be sure to unset the primary
        // bitstream.
        if (b.getID() == getPrimaryBitstreamID()) 
        {
            unsetPrimaryBitstreamID();
        }
        
        // Delete the mapping row
        DatabaseManager.updateQuery(ourContext,
                "DELETE FROM bundle2bitstream WHERE bundle_id= ? "+
                "AND bitstream_id= ? ", 
                getID(), b.getID());

        // If the bitstream is orphaned, it's removed
        TableRowIterator tri = DatabaseManager.query(ourContext,
                "SELECT * FROM bundle2bitstream WHERE bitstream_id= ? ",
                b.getID());

        try
        {
            if (!tri.hasNext())
            {
                // The bitstream is an orphan, delete it
                b.delete();
            }
        }
        finally
        {
            // close the TableRowIterator to free up resources
            if (tri != null)
            {
                tri.close();
            }
        }
    }

    /**
     * Update the bundle metadata
     */
    public void update() throws SQLException, AuthorizeException
    {
        // Check authorisation
        //AuthorizeManager.authorizeAction(ourContext, this, Constants.WRITE);
        log.info(LogManager.getHeader(ourContext, "update_bundle", "bundle_id="
                + getID()));

        if (modified)
        {
            ourContext.addEvent(new Event(Event.MODIFY, Constants.BUNDLE, getID(), null));
            modified = false;
        }
        if (modifiedMetadata)
        {
            ourContext.addEvent(new Event(Event.MODIFY_METADATA, Constants.BUNDLE, getID(), null));
            modifiedMetadata = false;
        }

        DatabaseManager.update(ourContext, bundleRow);
    }

    /**
     * Delete the bundle. Bitstreams contained by the bundle are removed first;
     * this may result in their deletion, if deleting this bundle leaves them as
     * orphans.
     */
    void delete() throws SQLException, AuthorizeException, IOException
    {
        log.info(LogManager.getHeader(ourContext, "delete_bundle", "bundle_id="
                + getID()));

        ourContext.addEvent(new Event(Event.DELETE, Constants.BUNDLE, getID(), getName()));

        // Remove from cache
        ourContext.removeCached(this, getID());

        // Remove bitstreams
        Bitstream[] bs = getBitstreams();

        for (int i = 0; i < bs.length; i++)
        {
            removeBitstream(bs[i]);
        }

        // remove our authorization policies
        AuthorizeManager.removeAllPolicies(ourContext, this);

        // Remove ourself
        DatabaseManager.delete(ourContext, bundleRow);
    }

    /**
     * return type found in Constants
     */
    public int getType()
    {
        return Constants.BUNDLE;
    }
    
    /**
     * remove all policies on the bundle and its contents, and replace them with
     * the DEFAULT_BITSTREAM_READ policies belonging to the collection.
     * 
     * @param c
     *            Collection
     * @throws java.sql.SQLException
     *             if an SQL error or if no default policies found. It's a bit
     *             draconian, but default policies must be enforced.
     * @throws AuthorizeException
     */
    public void inheritCollectionDefaultPolicies(Collection c)
            throws java.sql.SQLException, AuthorizeException
    {
        List<ResourcePolicy> policies = AuthorizeManager.getPoliciesActionFilter(ourContext, c,
                Constants.DEFAULT_BITSTREAM_READ);

        // change the action to just READ
        // just don't call update on the resourcepolicies!!!
        Iterator<ResourcePolicy> i = policies.iterator();

        if (!i.hasNext())
        {
            throw new java.sql.SQLException("Collection " + c.getID()
                    + " has no default bitstream READ policies");
        }

        while (i.hasNext())
        {
            ResourcePolicy rp = (ResourcePolicy) i.next();
            rp.setAction(Constants.READ);
        }

        replaceAllBitstreamPolicies(policies);
    }
    
    /**
     * remove all of the policies for the bundle and bitstream contents and replace
     * them with a new list of policies
     * 
     * @param newpolicies -
     *            this will be all of the new policies for the bundle and
     *            bitstream contents
     * @throws SQLException
     * @throws AuthorizeException
     */
    public void replaceAllBitstreamPolicies(List<ResourcePolicy> newpolicies)
            throws SQLException, AuthorizeException
    {
        if (bitstreams != null && bitstreams.size() > 0)
        {
            for (Bitstream bs : bitstreams)
            {
                // change bitstream policies
                AuthorizeManager.removeAllPolicies(ourContext, bs);
                AuthorizeManager.addPolicies(ourContext, newpolicies, bs);
            }
        }
        // change bundle policies
        AuthorizeManager.removeAllPolicies(ourContext, this);
        AuthorizeManager.addPolicies(ourContext, newpolicies, this);
    }

    public List<ResourcePolicy> getBundlePolicies() throws SQLException
    {
        return AuthorizeManager.getPolicies(ourContext, this);
    }

    public List<ResourcePolicy> getBitstreamPolicies() throws SQLException
    {
        List<ResourcePolicy> list = new ArrayList<ResourcePolicy>();
        if (bitstreams != null && bitstreams.size() > 0)
        {
            for (Bitstream bs : bitstreams)
            {
                list.addAll(AuthorizeManager.getPolicies(ourContext, bs));
            }
        }
        return list;
    }
    
    public DSpaceObject getAdminObject(int action) throws SQLException
    {
        DSpaceObject adminObject = null;
        Item[] items = getItems();
        Item item = null;
        Collection collection = null;
        Community community = null;
        if (items != null && items.length > 0)
        {
            item = items[0];
            collection = item.getOwningCollection();
            if (collection != null)
            {
                Community[] communities = collection.getCommunities();
                if (communities != null && communities.length > 0)
                {
                    community = communities[0];
                }
            }
        }
        switch (action)
        {
        case Constants.REMOVE:
            if (AuthorizeConfiguration.canItemAdminPerformBitstreamDeletion())
            {
                adminObject = item;
            }
            else if (AuthorizeConfiguration.canCollectionAdminPerformBitstreamDeletion())
            {
                adminObject = collection;
            }
            else if (AuthorizeConfiguration
                    .canCommunityAdminPerformBitstreamDeletion())
            {
                adminObject = community;
            }
            break;
        case Constants.ADD:
            if (AuthorizeConfiguration.canItemAdminPerformBitstreamCreation())
            {
                adminObject = item;
            }
            else if (AuthorizeConfiguration
                    .canCollectionAdminPerformBitstreamCreation())
            {
                adminObject = collection;
            }
            else if (AuthorizeConfiguration
                    .canCommunityAdminPerformBitstreamCreation())
            {
                adminObject = community;
            }
            break;

        default:
            adminObject = this;
            break;
        }
        return adminObject;
    }
    
    public DSpaceObject getParentObject() throws SQLException
    {
        Item[] items = getItems();
       
        if (items != null && (items.length > 0 && items[0] != null))
        {
            return items[0];
        }
        else
        {
            return null;
        }
    }

    @Override
    public void updateLastModified()
    {

    }
}
