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


import com.arsdigita.bebop.BoxPanel;
import com.arsdigita.bebop.ColumnPanel;
import com.arsdigita.bebop.Container;
import com.arsdigita.bebop.Form;
import com.arsdigita.bebop.FormData;
import com.arsdigita.bebop.FormProcessException;
import com.arsdigita.bebop.Label;
import com.arsdigita.bebop.PageState;
import com.arsdigita.bebop.RequestLocal;
import com.arsdigita.bebop.event.FormProcessListener;
import com.arsdigita.bebop.event.FormSectionEvent;
import com.arsdigita.bebop.event.FormValidationListener;
import com.arsdigita.bebop.event.PrintEvent;
import com.arsdigita.bebop.event.PrintListener;
import com.arsdigita.bebop.form.Option;
import com.arsdigita.bebop.form.OptionGroup;
import com.arsdigita.bebop.form.SingleSelect;
import com.arsdigita.bebop.form.Submit;
import com.arsdigita.bebop.parameters.BigDecimalParameter;
import com.arsdigita.bebop.tree.TreeNode;
import com.arsdigita.bebop.util.SequentialMap;
import com.arsdigita.categorization.Category;
import com.arsdigita.categorization.CategoryCollection;
import com.arsdigita.categorization.CategoryTreeModelLite;
import com.arsdigita.cms.CMS;
import com.arsdigita.cms.ContentItem;
import com.arsdigita.cms.ui.authoring.BasicItemForm;
import com.arsdigita.cms.util.GlobalizationUtil;
import com.arsdigita.domain.DataObjectNotFoundException;
import com.arsdigita.kernel.ACSObject;
import com.arsdigita.kernel.ui.DataQueryTreeNode;
import com.arsdigita.persistence.DataQuery;
import com.arsdigita.persistence.OID;
import com.arsdigita.persistence.SessionManager;
import com.arsdigita.util.StringUtils;
import com.arsdigita.util.UncheckedWrapperException;
import org.apache.log4j.Logger;

import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.TooManyListenersException;

/**
 * This is an abstract class which displays the category assignment UI.
 *
 *  Displays two listboxes for assigning categories to items, with two
 * submit buttons to move categories back and forth. The left
 * listbox displays all available categories which have not been
 * assigned to the current item. The right listbox displays all categories
 * assigned to the current item.
 * <p>
 *
 *
 * @author Stanislav Freidin (sfreidin@arsdigita.com)
 * @version $Id: //cms/dev/src/com/arsdigita/cms/ui/CategoryForm.java#22 $
 */
public abstract class CategoryForm extends Form
    implements FormProcessListener, FormValidationListener {


    public static final String versionId = "$Id: //cms/dev/src/com/arsdigita/cms/ui/CategoryForm.java#22 $ by $Author: dennis $, $DateTime: 2004/04/07 16:07:11 $";

    private RequestLocal m_assigned;
    private Submit m_assign, m_remove;
    Label m_freeLabel, m_assignedLabel;

    private static final String SEPARATOR = ">";

    public static final String FREE = "free";
    public static final String ASSIGNED = "assigned";
    public static final String ASSIGN = "assign";
    public static final String REMOVE = "remove";
    public static final int SELECT_WIDTH = 30;
    public static final int SELECT_HEIGHT = 10;
    public static final String FILLER_OPTION = StringUtils.repeat("_", SELECT_WIDTH);

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

    /**
     * Construct a new CategoryForm component
     *
     * @param name the name of the form
     */
    public CategoryForm(String name) {
        super(name, new ColumnPanel(3));

        ColumnPanel panel = (ColumnPanel)getPanel();
        panel.setBorder(false);
        panel.setPadColor("#FFFFFF");
        panel.setColumnWidth(1, "0%");
        panel.setColumnWidth(2, "0%");
        panel.setColumnWidth(3, "0");
        panel.setWidth("0%");
        panel.setClassAttr("CMS Admin");

        // Create the request local
        m_assigned = new RequestLocal() {
                public Object initialValue(PageState state) {
                    CategoryMap m = new CategoryMap();
                    initAssignedCategories(state, m);
                    return m;
                }
            };

        // Top row
        m_freeLabel = new Label(GlobalizationUtil.globalize("cms.ui.item.categories.available"),  false);
        m_freeLabel.setFontWeight(Label.BOLD);
        add(m_freeLabel, ColumnPanel.LEFT);

        add(new Label("&nbsp;", false));

        m_assignedLabel = new Label(GlobalizationUtil.globalize("cms.ui.item.categories.assigned"),  false);
        m_assignedLabel.setFontWeight(Label.BOLD);
        add(m_assignedLabel, ColumnPanel.LEFT);

        // Middle Row
        SingleSelect freeWidget =
            new SingleSelect(new BigDecimalParameter(FREE));
        try {
            freeWidget.addPrintListener(new FreePrintListener());
        } catch (TooManyListenersException e) {
            UncheckedWrapperException.throwLoggedException(getClass(), "Too many listeners", e);
        }
        freeWidget.setSize(SELECT_HEIGHT);
        add(freeWidget);

        BoxPanel box = new BoxPanel(BoxPanel.VERTICAL, true);
        box.setWidth("2%");
        addSubmitButtons(box);
        add(box, ColumnPanel.CENTER | ColumnPanel.MIDDLE);

        SingleSelect assignedWidget =
            new SingleSelect(new BigDecimalParameter(ASSIGNED));
        try {
            assignedWidget.addPrintListener(new AssignedPrintListener());
        } catch (TooManyListenersException e) {
            UncheckedWrapperException.throwLoggedException(getClass(), "Too many listeners", e);
        }
        assignedWidget.setSize(SELECT_HEIGHT);
        add(assignedWidget);

        // Add listeners
        addProcessListener(this);
        addValidationListener(this);

        setClassAttr("CategoryForm");
    }

    protected void addSubmitButtons(Container c) {
        addAssignButton(c);
        addRemoveButton(c);
    }

    protected void addAssignButton(Container c) {
        m_assign = new Submit(ASSIGN, ">>");
        m_assign.setSize(10);
        c.add(m_assign);
    }
    protected void addRemoveButton(Container c) {
        m_remove = new Submit(REMOVE, "<<");
        m_remove.setSize(10);
        c.add(m_remove);
    }
    /**
     * Set the caption of the unassigned categories label
     *
     * @param caption the new caption
     */
    public void setUnassignedCaption(String caption) {
        m_freeLabel.setLabel(caption);
    }

    /**
     * Set the caption of the assigned categories label
     *
     * @param caption the new caption
     */
    public void setAssignedCaption(String caption) {
        m_assignedLabel.setLabel(caption);
    }

    /**
     * @param s the page state
     * @return a {@link CategoryMap} of all assigned categories
     */
    public CategoryMap getAssignedCategories(PageState s) {
        return (CategoryMap)m_assigned.get(s);
    }

    // A print listener which populates the listbox with all
    // unassigned categories
    //
    // WARNING: This method is currently slow. It should be
    // optimized to do a connect by query that excludes all
    // categories which are already assigned.
    private class FreePrintListener implements PrintListener {

        public void prepare(PrintEvent e) {
            OptionGroup o = (OptionGroup)e.getTarget();
            PageState state = e.getPageState();
            Category root = getRootCategory(state);
            if(root == null)
                return;
            Category excluded = getExcludedCategory(state);

            // Breadth-first traversal of the teee
            CategoryTreeModelLite model = new CategoryTreeModelLite(root);
            CategoryMap assigned = getAssignedCategories(state);
            LinkedList queue = new LinkedList(), nameQueue = new LinkedList();

            if (root.isAbstract()) {
                queue.addLast(model.getRoot(state));
                nameQueue.addLast("");
            } else {
                queue.addLast(new DataQueryTreeNode
                              (root.getID(), root.getName(), true));
                nameQueue.addLast(root.getName());
            }
            while(!queue.isEmpty()) {
                DataQueryTreeNode node = (DataQueryTreeNode)queue.removeFirst();
                String name = (String)nameQueue.removeFirst();

                // Process the node
                String id = (String)node.getKey();

                // Process the node unless:
                // The category is assigned
                // The category's name is empty (meaning that it's the root)
                // The category should be excluded
                if(excluded == null ||
                   !excluded.getID().toString().equals(id)) {
                    if(!assigned.containsKey(id) && name.length() > 0 &&
                       !Boolean.TRUE.equals((Boolean)node.getValue
                                            (Category.IS_ABSTRACT)) &&
                       !id.equals(root.getID().toString())) {
                        o.addOption(new Option(id, name));
                    }

                    if (model.hasChildren(node, state)) {
                        // Append children
                        for(Iterator i = model.getChildren(node, state); i.hasNext(); ) {
                            TreeNode n = (TreeNode)i.next();
                            queue.addLast(n);
                            StringBuffer nameBuf = new StringBuffer(name);
                            if(name.length() > 0) {
                                nameBuf.append(SEPARATOR);
                            }
                            nameBuf.append(n.getElement());
                            nameQueue.addLast(nameBuf.toString());
                        }
                    }
                }

            }

            addFillerOption(o);
        }
    }

    /**
     * Populate a {@link CategoryMap} with all categories which are assigned to
     * the item. Child classes should override this method to do the right thing.
     *
     * @param map The sequential map of all categories which are assigned to
     *   the current item. Overridden method should repeatedly
     *   <code>call map.addCategory(someCategory);</code>
     * @param state The page state
     */
    protected abstract void initAssignedCategories(
                                                   PageState state, CategoryMap map
                                                   );

    /**
     * Assign a category, moving it from the list on the left
     * to the list on the right
     *
     * @param s the page state
     * @param cat the category to assign
     */
    protected abstract void assignCategory(PageState s, Category cat);

    /**
     * Unassign a category, moving it from the list on the right
     * to the list on the left
     *
     * @param s the page state
     * @param cat the category to unassign
     */
    protected abstract void unassignCategory(PageState s, Category cat);

    /**
     *  This method returns the URL for the givne item to make sure that
     *  the item it is not possible to have two objects in the same category
     *  with the same URL.
     *  @param state The Page State
     */
    protected abstract String getItemURL(PageState state);

    /**
     *  This allows the validation code to validate the properties of the
     *  object
     */
    protected abstract ACSObject getObject(PageState state);

    /**
     * Get the category which will act as the root for the lists
     * of assigned and unassigned categories. The default implementation
     * returns the root category for the content section. Child classes
     * should override this method if they wish to provide an alternate root category.
     *
     * @param state the page state
     * @return the root category which should be used to populate the lists
     *   of assigned and unassigned categories
     */
    public Category getRootCategory(PageState state) {
        return CMS.getContext().getContentSection().getRootCategory();
    }

    /**
     * Return a category which should be excluded from the list of
     * free categories. It is permissible to return null
     *
     * @param s the page state
     * @return a category whose subtree will not be shown in the
     *   category list
     */
    protected Category getExcludedCategory(PageState s) {
        return null;
    }

    // Populates the "assigned categories" widget
    private class AssignedPrintListener implements PrintListener {

        public void prepare(PrintEvent e) {
            OptionGroup o = (OptionGroup)e.getTarget();
            PageState state = e.getPageState();
            CategoryMap m = getAssignedCategories(state);

            if(!m.isEmpty()) {
                for (Iterator i = m.values().iterator(); i.hasNext(); ) {
                    Category c = (Category)i.next();
                    o.addOption(new Option(c.getID().toString(), getCategoryPath(c)));
                }
            } else {
                o.addOption(new Option("", "-- none --"));
            }

            addFillerOption(o);
        }
    }

    // Process the form: assign/unassign categories
    public void process(FormSectionEvent e) throws FormProcessException {
        PageState state = e.getPageState();
        FormData data = e.getFormData();
        BigDecimal id;

        if(m_assign.isSelected(state)) {
            id = (BigDecimal) data.get(FREE);

            // Assign a new category
            try {
                Category cat = new Category(
                    new OID(Category.BASE_DATA_OBJECT_TYPE, id));
                if (!cat.canMap()) {
                    data.addError(
                        (String) GlobalizationUtil.globalize(
                            "cms.ui.need_category_map_privilege").localize());
                    return;
                }
                assignCategory(state, cat);
                // Highlight the item
                data.put(ASSIGNED, id);
            } catch (DataObjectNotFoundException ex) {
                s_log.error("Couldn't find Category", ex);
                throw new FormProcessException(ex);
            }

        } else if(m_remove.isSelected(state)) {
            id = (BigDecimal) data.get(ASSIGNED);

            // Unassign a category
            try {
                Category cat = new Category(
                    new OID(Category.BASE_DATA_OBJECT_TYPE, id));
                if (!cat.canMap()) {
                    data.addError(
                        (String) GlobalizationUtil.globalize(
                            "cms.ui.need_category_map_privilege").localize());
                    return;
                }
                unassignCategory(state, cat);
                // Highlight the item
                data.put(FREE, id);
            } catch (DataObjectNotFoundException ex) {
                s_log.error("Couldn't find category");
                throw new FormProcessException(ex);
            }
        }
    }

    // Validate the form: make sure that a category is selected
    // for the remove/assign buttons
    public void validate(FormSectionEvent e) throws FormProcessException {
        PageState state = e.getPageState();
        FormData data = e.getFormData();

        if(m_assign.isSelected(state)) {
            if(data.get(FREE) == null) {
                data.addError("Please select a category to assign");
            } else {
                // we need to make sure that no other item in this
                // category has the same name (url)
                BigDecimal id = (BigDecimal) data.get(FREE);

                // Assign a new category
                try {
                    Category cat =
                        new Category(new OID(Category.BASE_DATA_OBJECT_TYPE, id));
                    String url = getItemURL(state);

                    if (url != null) {
                        DataQuery query = SessionManager.getSession().retrieveQuery
                            ("com.arsdigita.categorization.getAllItemURLsForCategory");
                        query.setParameter("categoryID", id);
                        query.addEqualsFilter("lower(url)", url.toLowerCase());

                        if (query.size() > 0) {
                            // we need to make sure that there is not an item
                            ACSObject item = getObject(state);
                            Collection list = null;
                            if (item instanceof ContentItem) {
                                list = BasicItemForm.getAllVersionIDs
                                    ((ContentItem)item);
                            } else {
                                list = new ArrayList();
                                list.add(item.getID());
                            }
                            BigDecimal itemID = null;
                            while (query.next()) {
                                itemID = (BigDecimal)query.get("itemID");
                                if (!list.contains(itemID)) {
                                    data.addError("There is already an item " +
                                                  "with the url " + url +
                                                  " in the category " +
                                                  cat.getName());
                                    break;
                                }
                            }
                        }
                    }
                } catch (DataObjectNotFoundException ex) {
                    s_log.error("Error processing category.  Unable to find " +
                                "category with id " + id);
                    throw new FormProcessException(ex);
                }
            }
        } else if(m_remove.isSelected(state)) {
            if(data.get(ASSIGNED) == null) {
                data.addError("Please select a category to remove");
            }
        }
    }

    // Add a "filler" option to the option group in order to ensure
    // the correct horizontal width
    private static void addFillerOption(OptionGroup o) {
        o.addOption(new Option("", FILLER_OPTION));
    }

    /**
     * @return the full path to a category
     */
    public static String getCategoryPath(Category c) {
        final StringBuffer s = new StringBuffer();
        final CategoryCollection ancestors = c.getDefaultAscendants();
        ancestors.addOrder(Category.DEFAULT_ANCESTORS);

        boolean isFirst = true;

        while(ancestors.next()) {
            if ( !isFirst ) {
                s.append(SEPARATOR);
            }
            s.append(ancestors.getCategory().getName());
            isFirst = false;
        }

        return s.toString();
    }

    /**
     * A convenience method that abstracts SequentialMap
     * to deal with categories
     */
    protected static class CategoryMap extends SequentialMap {

        public CategoryMap() {
            super();
        }

        public void add(Category c) {
            super.put(c.getID().toString(), c);
        }
    }



}
