/*
 * Copyright (C) 2001-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.formbuilder.util;


import java.math.BigDecimal;

// Interface implemented by all persistent containers
import com.arsdigita.formbuilder.PersistentContainer;

// Interface of the object using this helper
import com.arsdigita.formbuilder.PersistentComponentFactory;

// Interaction with the persistence layer
// For lack of Link attribute support in the persistence layer
// I use queries and data operations instead of an association
import com.arsdigita.persistence.Session;
import com.arsdigita.persistence.SessionManager;
import com.arsdigita.persistence.DataQuery;
import com.arsdigita.persistence.DataOperation;

// For instantiating objects
import com.arsdigita.formbuilder.util.FormBuilderUtil;

// Child components are returned in a collection
import java.util.Collection;
import java.util.List;
import java.util.ArrayList;

// ACS 5 uses Log4J for logging
import org.apache.log4j.Logger;


/**
 * This class is used internally by PersistentFormSection and PersistentOptionGroup
 * to manage the associations to the Component children of these containers.
 *
 * @author Peter Marklund
 * @version $Id: //core-platform/dev/src/com/arsdigita/formbuilder/util/PersistentContainerHelper.java#9 $
 *
 */
public class PersistentContainerHelper
    implements PersistentContainer {

    public static final String versionId = "$Id: //core-platform/dev/src/com/arsdigita/formbuilder/util/PersistentContainerHelper.java#9 $ by $Author: dennis $, $DateTime: 2004/04/07 16:07:11 $";

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

    private PersistentComponentFactory m_factory;

    // We cache the number of components to avoid querying the
    // database about this number everytime a component is added
    // A value of -1 indicates that the field is not initialized
    // We are using lazy initialization here
    private int m_numberOfComponents = -1;

    private List m_selectedComponents = new ArrayList();

    public PersistentContainerHelper(PersistentComponentFactory factory) {

        m_factory = factory;
    }

    // *** Public API

    /**
     * Add a component after the already added components (in the last position).
     * If this domain object has not been saved (with save()) before you invoke this method
     * it will be saved by this method. The component will not be selected.
     */
    public void addComponent(PersistentComponentFactory componentFactory) {

        addComponent(componentFactory, getNumberOfComponents() + 1, false);
    }

    /**
     * Add a component after the already added components (in the last position).
     * If this domain object has not been saved (with save()) before you invoke this method
     * it will be saved by this method.
     */
    public void addComponent(PersistentComponentFactory componentFactory, boolean selected) {

        addComponent(componentFactory, getNumberOfComponents() + 1, selected);
    }

    /**
     * Add a child component of the container.
     * If this domain object has not been saved (with save()) before you invoke this method
     * it will be saved by this method. The component will not be selected.
     *
     * @param position The count of this component starting with 1 (i.e. if it's
     *        the third component to be added to the container this
     *        value would be 3)
     */
    public void addComponent(PersistentComponentFactory componentFactory,
                             int position) {

        addComponent(componentFactory, position, false);
    }

    /**
     * Add a child component of the container.
     * If this domain object has not been saved (with save()) before you invoke this method
     * it will be saved by this method.
     *
     * @param position The count of this component starting with 1 (i.e. if it's
     *        the third component to be added to the container this
     *        value would be 3)
     */
    public void addComponent(PersistentComponentFactory componentFactory,
                             int position,
                             boolean isSelected) {

        // We need the container and the component to be saved for referential
        // integrity in the database
        if (m_factory.isNew()) {
            m_factory.save();
        }

        if (componentFactory.isNew()) {
            componentFactory.save();
        }

        assertPositionInAddRange(position);

        // Add the component
        executeAddComponent(componentFactory.getID(), position, isSelected);

        // Increment the component counter
        incrementNumberOfComponents();
    }

    /**
     * Remove a component from the container.
     * If this domain object has not been saved (with save()) before you invoke this method
     * it will be saved by this method.
     */
    public void removeComponent(PersistentComponentFactory componentFactory) {

        // We need the container to be saved for referential integrity in the database
        if (m_factory.isNew()) {
            m_factory.save();
        }
        if (componentFactory.isNew()) {
            throw new IllegalArgumentException("Trying to remove factory with id " + componentFactory.getID() +
                                               " that has not been saved. A factory must have been first added " +
                                               " and saved before it can be removed");
        }

        // Remove the component
        executeRemoveComponent(componentFactory.getID());

        // Decrement the component counter
        decrementNumberOfComponents();
    }

    /**
     * Move component to new position.
     *
     * @param toPosition The position to move the component to. Positions start with 1.
     */
    public void moveComponent(PersistentComponentFactory componentFactory,
                              int toPosition) {

        removeComponent(componentFactory);

        addComponent(componentFactory, toPosition);
    }

    public void clearComponents() {

        executeClearComponents();
    }

    public void setComponentSelected(PersistentComponentFactory componentFactory,
                                     boolean selected) {

        executeSetComponentSelected(componentFactory.getID(), selected);
    }

    /**
     * Return all children components represented by their PersistentComponentFactory
     * objects.
     */
    public Collection getComponents() {

        java.util.ArrayList componentList = new java.util.ArrayList();

        // Initialize the list of selected components
        m_selectedComponents = new ArrayList();

        Session session = SessionManager.getSession();
        DataQuery query =
            session.retrieveQuery("com.arsdigita.formbuilder.RetrieveComponents");

        // Retrieve only children of this container
        query.setParameter("containerID", m_factory.getID());

        // Loop over the child components
        while (query.next()) {
            BigDecimal componentID = (BigDecimal)query.get("componentID");
            String factoryClassName = query.get("defaultDomainClass").toString();
            Boolean isSelected = (Boolean)query.get("isSelected");

            // Instantiate the factory
            PersistentComponentFactory factory =
                (PersistentComponentFactory)FormBuilderUtil.instantiateObjectOneArg(factoryClassName, componentID);

            componentList.add(factory);

            // Add the factory to the list of selected components if it is selected
            if (isSelected.booleanValue()) {
                m_selectedComponents.add(factory);
            }
        }

        return componentList;
    }

    /**
     * Can be invoked after getComponents() has been invoked to fetch the components
     * that are selected. Returns a list of PersistentComponentFactory objects corresponding
     * to the selected components.
     */
    public List getSelectedComponents() {

        s_log.debug("getSelectedComponents class name is " + m_selectedComponents.getClass().getName() +
                    " size " + m_selectedComponents.size());

        return m_selectedComponents;
    }

    // *** Internal Helper Methods
    /**
     * Map a component with a certain position to the container.
     */
    protected void executeAddComponent(BigDecimal componentID,
                                       int position,
                                       boolean isSelected) {

        Session session = SessionManager.getSession();

        // First update the order numbers
        DataOperation operation =
            session.retrieveDataOperation("com.arsdigita.formbuilder.UpdateOrderBeforeAdd");
        operation.setParameter("containerID", m_factory.getID());
        operation.setParameter("orderNumber", new Integer(position));
        operation.execute();

        // Add the component
        operation = session.retrieveDataOperation("com.arsdigita.formbuilder.AddComponent");
        operation.setParameter("containerID", m_factory.getID());
        operation.setParameter("componentID", componentID);
        operation.setParameter("orderNumber", new Integer(position));
        operation.setParameter("isSelected", new Boolean(isSelected));
        operation.execute();
    }

    /**
     * Remove a component-container mapping. Note that this does not remove the component
     */
    protected void executeRemoveComponent(BigDecimal componentID) {
        Session session = SessionManager.getSession();

        // Update the order numbers first
        DataOperation operation =
            session.retrieveDataOperation("com.arsdigita.formbuilder.UpdateOrderBeforeRemove");
        operation.setParameter("containerID", m_factory.getID());
        operation.setParameter("componentID", componentID);
        operation.execute();

        // Remove the component
        operation = session.retrieveDataOperation("com.arsdigita.formbuilder.RemoveComponent");
        operation.setParameter("containerID", m_factory.getID());
        operation.setParameter("componentID", componentID);
        operation.execute();
    }

    /**
     * Remove all component associations from the container
     */
    protected void executeClearComponents() {

        Session session = SessionManager.getSession();
        DataOperation operation =
            session.retrieveDataOperation("com.arsdigita.formbuilder.ClearComponents");

        operation.setParameter("containerID", m_factory.getID());

        operation.execute();
    }

    protected void executeSetComponentSelected(BigDecimal componentID, boolean selected) {

        Session session = SessionManager.getSession();
        DataOperation operation =
            session.retrieveDataOperation("com.arsdigita.formbuilder.SetComponentSelected");

        operation.setParameter("containerID", m_factory.getID());
        operation.setParameter("componentID", componentID);
        operation.setParameter("isSelected", new Boolean(selected));
        operation.execute();

    }

    /**
     * Assert that the position is valid for adding a component at.
     */
    protected void assertPositionInAddRange(int position) {
        // +2 not +1, since component numbers start at '1', and we
        // need to be able to add after the last component.
        assertPositionInRange(position, getNumberOfComponents() + 2);
    }

    /**
     * Assert that the position is within the current range of component positions.
     */
    protected void assertPositionInCurrentRange(int position) {

        assertPositionInRange(position, getNumberOfComponents());
    }

    /**
     * Assert that the position is between 1 and the given upper limit
     */
    protected void assertPositionInRange(int position, int upperLimit) {

        try {
            FormBuilderUtil.assertArgumentInRange(position, 1, upperLimit);

        } catch (Exception e) {

            throw new IllegalArgumentException("position " + Integer.toString(position) +
                                               " provided to " + this.toString() + " is invalid" +
                                               ", should be between 1 and " +
                                               Integer.toString(upperLimit));
        }
    }

    protected int getNumberOfComponents() {

        // Initialize the cached number if this has not been done
        if (m_numberOfComponents == -1) {

            // Get number from database and cache it
            Session session = SessionManager.getSession();
            DataQuery query =
                session.retrieveQuery("com.arsdigita.formbuilder.NumberOfComponents");
            query.setParameter("containerID", m_factory.getID());
            query.next();
            m_numberOfComponents =
                ((Integer)query.get("numberOfComponents")).intValue();
            query.close();
        }

        return m_numberOfComponents;
    }

    protected void incrementNumberOfComponents() {

        // Initialize the number if this has not been done
        if (m_numberOfComponents == -1) {
            getNumberOfComponents();
        }

        // Increment the number
        ++m_numberOfComponents;
    }

    protected void decrementNumberOfComponents() {

        // Initialize the number if this has not been done
        if (m_numberOfComponents == -1) {
            getNumberOfComponents();
        }

        // Decrement the counter
        --m_numberOfComponents;
    }
}
