/*
 * Copyright (C) 2003-2004 Red Hat Inc. All Rights Reserved.
 *
 * The contents of this file are subject to the CCM Public
 * License (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.redhat.com/licenses/ccmpl.html.
 *
 * Software distributed under the License is distributed on an
 * "AS IS" basis, WITHOUT WARRANTY OF ANY KIND, either express
 * or implied. See the License for the specific language
 * governing rights and limitations under the License.
 *
 */
package com.arsdigita.cms;

import com.arsdigita.categorization.Category;
import com.arsdigita.categorization.CategoryCollection;
import com.arsdigita.cms.lifecycle.Lifecycle;
import com.arsdigita.cms.lifecycle.LifecycleDefinition;
import com.arsdigita.domain.DataObjectNotFoundException;
import com.arsdigita.domain.DomainObjectFactory;
import com.arsdigita.persistence.DataAssociation;
import com.arsdigita.persistence.DataAssociationCursor;
import com.arsdigita.persistence.DataObject;
import com.arsdigita.persistence.OID;
import com.arsdigita.persistence.metadata.Property;
import com.arsdigita.util.Assert;
import com.arsdigita.web.Web;
import com.arsdigita.workflow.simple.Workflow;
import com.arsdigita.workflow.simple.WorkflowTemplate;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.Enumeration;
import java.util.List;
import java.util.Locale;

import org.apache.log4j.Logger;

/**
 * A bundle of content items of different languages.  A bundle ties
 * the various language instances of an item together and provides
 * methods to access them.
 *
 * @author Shashin Shinde
 * @author Justin Ross &lt;jross@redhat.com&gt;
 * @version $Id: //cms/dev/src/com/arsdigita/cms/ContentBundle.java#32 $
 */
public class ContentBundle extends ContentItem {
    public static final String versionId =
        "$Id: //cms/dev/src/com/arsdigita/cms/ContentBundle.java#32 $" +
        "$Author: dennis $" +
        "$DateTime: 2004/04/07 16:07:11 $";

    private static final Logger s_log = Logger.getLogger(ContentBundle.class);

    /**
     * The base data object type of a bundle
     */
    public static final String BASE_DATA_OBJECT_TYPE =
        "com.arsdigita.cms.ContentBundle";

    /**
     * The primary instances association
     */
    public static final String INSTANCES = "instances";

    /**
     * The default language property
     */
    public static final String DEFAULT_LANGUAGE = "defaultLanguage";

    private boolean m_wasNew = false;

    /**
     * Returns the data object type for this bundle.
     */
    public String getBaseDataObjectType() {
        return BASE_DATA_OBJECT_TYPE;
    }

    /**
     * Creates a new bundle.
     *
     * @param primary The primary language instance of this bundle
     */
    public ContentBundle(final ContentItem primary) {
        super(BASE_DATA_OBJECT_TYPE);

        Assert.exists(primary, ContentItem.class);

        setDefaultLanguage(primary.getLanguage());
        setContentType(primary.getContentType());
        addInstance(primary);

        super.setName(primary.getName());
    }

    /**
     * Retrieves a bundle.
     *
     * @param oid the <code>OID</code> of the bundle to retrieve
     */
    public ContentBundle(final OID oid) throws DataObjectNotFoundException {
        super(oid);
    }

    /**
     * Retrieves a bundle.
     *
     * @param id the <code>BigDecimal</code> id of the bundle to
     * retrieve
     */
    public ContentBundle(final BigDecimal id)
            throws DataObjectNotFoundException {
        this(new OID(BASE_DATA_OBJECT_TYPE, id));
    }

    /**
     * Retrieves or creates a bundle using the <code>DataObject</code>
     * argument.
     *
     * @param object the <code>DataObject</code> to use in creating or
     * retrieving the bundle
     */
    public ContentBundle(final DataObject object) {
        super(object);
    }

    /**
     * Creates a bundle.
     *
     * @param type the <code>String</code> data object type with which
     * to create a new bundle
     */
    public ContentBundle(final String type) {
        super(type);
    }


    protected ContentItem makeCopy() {
        final ContentBundle newItem = (ContentBundle) super.makeCopy();

        final WorkflowTemplate template =
            ContentTypeWorkflowTemplate.getWorkflowTemplate
                (newItem.getContentSection(), newItem.getContentType());

        if (template != null) {
            s_log.debug("Setting up new workflow template");
            ItemCollection instances = getInstances();
            while (instances.next()) {
                ContentItem instance = instances.getContentItem();
                s_log.debug("Item id is: " + instance.getID());
                final Workflow workflow = template.instantiateNewWorkflow();
                workflow.setObjectID(instance.getID());
                workflow.start(Web.getContext().getUser());
                workflow.save();

            }
        }

        return newItem;
    }

    /**
     * Gets the default language of the bundle.
     */
    public final String getDefaultLanguage() {
        return (String) get(DEFAULT_LANGUAGE);
    }

    /**
     * Sets the default language of the bundle.
     */
    public final void setDefaultLanguage(final String language) {
        if (Assert.isEnabled()) {
            Assert.exists(language, String.class);
            Assert.truth(language.length() == 2,
                         language + " is not an ISO639 language code");
        }

        set(DEFAULT_LANGUAGE, language);
    }

    /**
     * Adds a language instance to this bundle.  This method will fail
     * if the bundle already contains a different instance for the
     * same language.
     *
     * Note that in order to set the primary instance you must call
     * this method and {@link #setDefaultLanguage(String)} as well.
     *
     * @param instance the new language instance
     * @see #setDefaultLanguage(String)
     * @pre instance != null
     * @post this.equals(instance.getParent())
     */
    public void addInstance(final ContentItem instance) {
        if (s_log.isDebugEnabled()) {
            s_log.debug("Adding " + instance + " to bundle " + this);
        }

        if (Assert.isEnabled()) {
            Assert.exists(instance, ContentItem.class);
            Assert.falsity(hasInstance(instance.getLanguage()),
                           "The bundle already contains an instance " +
                           "for the language " + instance.getLanguage());
        }

        instance.setParent(this);
        instance.setContentSection(getContentSection());

        if (Assert.isEnabled()) {
            Assert.equal(this, instance.getParent());
        }
    }

    /**
     * Removes a language instance from the bundle.  This method will
     * fail if <code>instance</code> is the primary instance.
     *
     * Note that the language instance is not deleted by this
     * operation; it is just removed from the Bundle. Users of this
     * method have to take care to properly dispose of this instance!
     *
     * @param instance The language instance to remove
     * @pre instance != null
     * @post instance.getParent() == null
     */
    public void removeInstance(final ContentItem instance) {
        if (Assert.isEnabled()) {
            Assert.exists(instance, ContentItem.class);
            Assert.equal(this, instance.getParent());
            Assert.unequal(instance, getPrimaryInstance());
        }

        instance.setParent(null);

        if (Assert.isEnabled()) {
            Assert.truth(instance.getParent() == null);
        }
    }

    /**
     * Gets the primary instance of this bundle.
     *
     * @return the language instance of this item which is marked as
     * the primary instance
     * @see #addInstance(ContentItem)
     */
    public final ContentItem getPrimaryInstance() {
        return getInstance(getDefaultLanguage());
    }

    /**
     * Produces a collection containing all language instances in this
     * bundle.
     *
     * @return a collection of language instances
     */
    public final ItemCollection getInstances() {
        return new ItemCollection(instances());
    }

    /**
     * Returns a language instance for <code>language</code> or
     * <code>null</code> if no such instance exists.
     *
     * This method does <strong>not</strong> do language negotiation,
     * it only returns an exact match for the given Locale or
     * <code>null</code> if no such match is found.
     *
     * @param language the language for which to get an instance
     * @return the instance of this item which exactly matches the
     * <em>language</em> part of the Locale <code>l</code>
     * @see #negotiate
     * @pre language != null
     */
    public final ContentItem getInstance(final String language) {
        if (Assert.isEnabled()) {
            Assert.exists(language, String.class);
            Assert.truth(language.length() == 2,
                         language + " does not look like a valid language " +
                         "code");
        }

        final DataAssociationCursor instances = instances();
        instances.addEqualsFilter(LANGUAGE, language);

        DataObject dataObject = null;

        if (instances.next()) {
            final DataObject data = instances.getDataObject();

            if (Assert.isEnabled()) {
                //Assert.falsity(instances.next(),
                //               "There is more than one instance with the " +
                //               "same language");
            }

            instances.close();

            return (ContentItem) DomainObjectFactory.newInstance(data);
        } else {
            instances.close();

            return null;
        }
    }

    /**
     * Tells whether <code>instance</code> is present in the bundle.
     *
     * @param instance the language instance to look for
     * @return <code>true</code> if the instance is in the bundle
     */
    public final boolean hasInstance(final ContentItem instance) {
        Assert.exists(instance, ContentItem.class);

        final DataAssociationCursor instances = instances();
        instances.addEqualsFilter(ID, instance.getID());

        return !instances.isEmpty();
    }

    /**
     * Utility method to check if this bundle already contains an
     * instance for the given <code>language</code>.
     *
     * @param language an ISO639 2-letter language code
     * @return <code>true</code> if this <code>ContentBundle</code>
     * contains an instance for the language given as an argument
     * @see ContentItem#getLanguage()
     */
    public final boolean hasInstance(final String language) {
        if (Assert.isEnabled()) {
            Assert.exists(language, String.class);
            Assert.truth(language.length() == 2,
                         language + " is not an ISO639 language code");
        }

        final DataAssociationCursor instances = instances();
        instances.addEqualsFilter(LANGUAGE, language);

        return !instances.isEmpty();
    }

    /**
     * List all languages in which this item is available, i.e. the
     * language codes of all instances in this bundle.
     *
     * @return A <code>Collection</code> of language 2-letter codes in
     * which this item is available
     */
    public final Collection getLanguages() {
        // XXX For LIVE bundles, there might be several PENDING
        // instances with the same language. Maybe we should filter
        // these out and return only one?

        final ItemCollection items = getInstances();

        final Collection list = new ArrayList();

        while (items.next()) {
            list.add(items.getLanguage());
        }

        items.close();

        if (Assert.isEnabled()) {
            Assert.truth(!list.isEmpty() || getInstances().isEmpty());
        }

        return list;
    }

    /**
     * Negotiate the right language instance for this bundle and return it.
     *
     * @param locales the acceptable locales for the language
     * instance, in <em>decreasing</em> importance
     * @return the negotiated language instance or <code>null</code>
     * if there is no language instance for any of the locales in
     * <code>locales</code>
     * @pre locales != null
     */
    public ContentItem negotiate(Locale[] locales) {
        Assert.assertNotNull(locales);
        DataAssociationCursor instancesCursor = instances();
        DataObject dataObject = null;
        int bestMatch = 0;
        DataObject matchingInstance = null;
        String language = null;
        while (instancesCursor.next()) {
            dataObject = instancesCursor.getDataObject();
            language = (String) dataObject.get(LANGUAGE);

            if (s_log.isDebugEnabled()) {
                s_log.debug("negotiate: language= " + language);
            }

            if (language != null) {
                for (int i=0; i < locales.length; i++) {
                    if (language.equals(locales[i].getLanguage())) {
                        if (i < bestMatch || matchingInstance == null) {
                            bestMatch = i;
                            matchingInstance = dataObject;
                            if (s_log.isDebugEnabled()) {
                                s_log.debug("negotiate: "
                                            + "bestMatch= " + i
                                            + ", language= " + language);
                            }
                        } // else other match with less preferred language found
                    }
                } // end for
            } // end if
            if (bestMatch == 0 && matchingInstance != null) {
                s_log.debug("negotiate: best possible match found, exiting");
                break;       // exit loop when best match is found
            }
        }
        instancesCursor.close();
        if (matchingInstance != null) {
            return (ContentItem) DomainObjectFactory.newInstance
                (matchingInstance);
        } else {
            s_log.info("negotiate: no match found!");
            return null;
        }
    }

    /**
     * Negotiate the right language instance for this bundle and return it.
     *
     * @param locales the acceptable locales for the language instance, in
     *  <em>decreasing</em> importance. This parameter has to be an
     * <code>Enumeration</code> of <code>Locale</code> objects.
     * @return the negotiated language instance or <code>null</code> if there
     *  is no language instance for any of the locales in <code>locales</code>.
     * @pre locales != null
     */
    public ContentItem negotiate(Enumeration locales) {
        Assert.assertNotNull(locales);
        /* copy "locales" enumeration, since we have to iterate
         * over it several times
         */
        Locale loc = null;
        List languageCodes = new ArrayList();
        for (int i = 0; locales.hasMoreElements(); i++) {
            loc = (Locale)locales.nextElement();
            languageCodes.add( loc.getLanguage());
            if (s_log.isDebugEnabled()) {
                s_log.debug("negotiate: pref " + i + ": "+ loc.getLanguage());
            }
        }

        final DataAssociationCursor instances = instances();

        DataObject dataObject = null;
        int bestMatch = 0;
        DataObject match = null;
        String language = null;

        while (instances.next()) {
            dataObject = instances.getDataObject();
            language = (String) dataObject.get(LANGUAGE);

            if (s_log.isDebugEnabled()) {
                s_log.debug("negotiate: language= " + language);
            }

            if (language != null) {
                for (int i=0; i < languageCodes.size(); i++) {
                    if (language.equals( (String)languageCodes.get(i) )) {
                        if (i < bestMatch || match == null) {
                            bestMatch = i;
                            match = dataObject;
                            if (s_log.isDebugEnabled()) {
                                s_log.debug("negotiate: "
                                            + "bestMatch= " + i
                                            + ", language= " + language);
                            }
                        } // else other match with less preferred language found
                    }
                } // end for
            } // end if

            if (bestMatch == 0 && match != null) {
                s_log.debug("negotiate: best possible match found, exiting");
                break;       // exit loop when best match is found
            }
        }

        instances.close();

        return (ContentItem) DomainObjectFactory.newInstance(match);
    }

    // Methods from item that bundle overrides

    protected void beforeSave() {
        super.beforeSave();

        final ContentItem primary = getPrimaryInstance();

        Assert.exists(getContentType(), ContentType.class);

        if (primary != null) {
            primary.setContentSection(getContentSection());
        }
    }

    protected boolean canPublishToFS() {
        return false;
    }

    protected void publishToFS() {
        throw new UnsupportedOperationException();
    }

    public ContentItem publish(final LifecycleDefinition definition,
                               final Date start) {
        throw new UnsupportedOperationException();
    }

    public Lifecycle getLifecycle() {
        // Bundles do not have lifecycles.

        return null;
    }

    public void setLifecycle(final Lifecycle lifecycle) {
        // I'd like to do the following, but VersionCopier calls
        // setLifecycle.
        //throw new UnsupportedOperationException();
    }


    /**
     * Ignore the <code>INSTANCES</code> property for 
     * <code>ItemCopier.VERSION_COPY</code>.
     *
     * @param source the source CustomCopy item
     * @param property the property to copy
     * @param copier a temporary class that is able to copy a child item
     *   correctly.
     * @return true if the property was copied; false to indicate
     *   that regular metadata-driven methods should be used
     *   to copy the property.
     */
    public boolean copyProperty(final CustomCopy source,
                                   final Property property,
                                   final ItemCopier copier) {
        if (copier.getCopyType() == ItemCopier.VERSION_COPY
                && INSTANCES.equals(property.getName())) {
            return true;
        }

        return super.copyProperty(source, property, copier);
    }

    public boolean copyServices(final ContentItem source) {
        if (s_log.isDebugEnabled()) {
            s_log.debug("Copying services on bundle " + getName() + " " +
                        getID() + " using source " + source.getID());
        }

        // Copy categories

        CategoryCollection categories = source.getCategoryCollection();
        while (categories.next() ) {
            final Category category = categories.getCategory();
            category.addChild(this);
            category.save(); // XXX remove me
        }
        categories.close();

        return true;
    }

    protected void initialize() {
        super.initialize();

        m_wasNew = isNew();
    }

    protected void afterSave() {
        if (m_wasNew) {
            getPrimaryInstance().setContentSection(getContentSection());
        }

        super.afterSave();
    }

    // Utility methods

    private DataAssociationCursor instances() {
        final DataAssociationCursor cursor =
            ((DataAssociation) super.get(INSTANCES)).cursor();

        return cursor;
    }

    private DataAssociationCursor instances(final String version) {
        final DataAssociationCursor cursor = instances();

        cursor.addEqualsFilter(VERSION, version);

        return cursor;
    }
}
