/*
 * Copyright (C) 2007 The Android Open Source Project
 *
 * Licensed under the Eclipse Public License, Version 1.0 (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.eclipse.org/org/documents/epl-v10.php
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.android.ide.eclipse.adt.internal.editors.ui.tree;

import com.android.ide.eclipse.adt.AdtPlugin;
import com.android.ide.eclipse.adt.internal.editors.AndroidXmlEditor;
import com.android.ide.eclipse.adt.internal.editors.descriptors.ElementDescriptor;
import com.android.ide.eclipse.adt.internal.editors.layout.descriptors.ViewElementDescriptor;
import com.android.ide.eclipse.adt.internal.editors.uimodel.UiElementNode;

import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.Status;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Shell;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.dialogs.AbstractElementListSelectionDialog;
import org.eclipse.ui.dialogs.ISelectionStatusValidator;
import org.eclipse.ui.part.FileEditorInput;

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.Map.Entry;

/**
 * A selection dialog to select the type of the new element node to
 * create, either in the application node or the selected sub node.
 */
public class NewItemSelectionDialog extends AbstractElementListSelectionDialog {

    /** The UI node selected in the tree view before creating the new item selection dialog.
     *  Can be null -- which means new items must be created in the root_node. */
    private UiElementNode mSelectedUiNode;
    /** The root node chosen by the user, either root_node or the one passed
     *  to the constructor if not null */
    private UiElementNode mChosenRootNode;
    private UiElementNode mLocalRootNode;
    /** The descriptor of the elements to be displayed as root in this tree view. All elements
     *  of the same type in the root will be displayed. Can be null. */
    private ElementDescriptor[] mDescriptorFilters;
    /** The key for the {@link #setLastUsedXmlName(Object[])}. It corresponds to the full
     * workspace path of the currently edited file, if this can be computed. This is computed
     * by {@link #getLastUsedXmlName(UiElementNode)}, called from the constructor. */
    private String mLastUsedKey;
    /** A static map of known XML Names used for a given file. The map has full workspace
     * paths as key and XML names as values. */
    private static final Map<String, String> sLastUsedXmlName = new HashMap<String, String>();
    /** The potential XML Name to initially select in the selection dialog. This is computed
     * in the constructor and set by {@link #setInitialSelection(UiElementNode)}. */
    private String mInitialXmlName;

    /**
     * Creates the new item selection dialog.
     *
     * @param shell The parent shell for the list.
     * @param labelProvider ILabelProvider for the list.
     * @param descriptorFilters The element allows at the root of the tree. Can be null.
     * @param ui_node The selected node, or null if none is selected.
     * @param root_node The root of the Ui Tree, either the UiDocumentNode or a sub-node.
     */
    public NewItemSelectionDialog(Shell shell, ILabelProvider labelProvider,
            ElementDescriptor[] descriptorFilters,
            UiElementNode ui_node,
            UiElementNode root_node) {
        super(shell, labelProvider);
        mDescriptorFilters = descriptorFilters;
        mLocalRootNode = root_node;

        // Only accept the UI node if it is not the UI root node and it can have children.
        // If the node cannot have children, select its parent as a potential target.
        if (ui_node != null && ui_node != mLocalRootNode) {
            if (ui_node.getDescriptor().hasChildren()) {
                mSelectedUiNode = ui_node;
            } else {
                UiElementNode parent = ui_node.getUiParent();
                if (parent != null && parent != mLocalRootNode) {
                    mSelectedUiNode = parent;
                }
            }
        }

        setHelpAvailable(false);
        setMultipleSelection(false);

        setValidator(new ISelectionStatusValidator() {
            public IStatus validate(Object[] selection) {
                if (selection.length == 1 && selection[0] instanceof ViewElementDescriptor) {
                    return new Status(IStatus.OK, // severity
                            AdtPlugin.PLUGIN_ID, //plugin id
                            IStatus.OK, // code
                            ((ViewElementDescriptor) selection[0]).getFullClassName(), //msg
                            null); // exception
                } else if (selection.length == 1 && selection[0] instanceof ElementDescriptor) {
                    return new Status(IStatus.OK, // severity
                            AdtPlugin.PLUGIN_ID, //plugin id
                            IStatus.OK, // code
                            "", //$NON-NLS-1$ // msg
                            null); // exception
                } else {
                    return new Status(IStatus.ERROR, // severity
                            AdtPlugin.PLUGIN_ID, //plugin id
                            IStatus.ERROR, // code
                            "Invalid selection", // msg, translatable
                            null); // exception
                }
            }
        });

        // Determine the initial selection using a couple heuristics.

        // First check if we can get the last used node type for this file.
        // The heuristic is that generally one keeps adding the same kind of items to the
        // same file, so reusing the last used item type makes most sense.
        String xmlName = getLastUsedXmlName(root_node);
        if (xmlName == null) {
            // Another heuristic is to find the most used item and default to that.
            xmlName = getMostUsedXmlName(root_node);
        }
        if (xmlName == null) {
            // Finally the last heuristic is to see if there's an item with a name
            // similar to the edited file name.
            xmlName = getLeafFileName(root_node);
        }
        // Set the potential name. Selecting the right item is done later by setInitialSelection().
        mInitialXmlName = xmlName;
    }

    /**
     * Returns a potential XML name based on the file name.
     * The item name is marked with an asterisk to identify it as a partial match.
     */
    private String getLeafFileName(UiElementNode ui_node) {
        if (ui_node != null) {
            AndroidXmlEditor editor = ui_node.getEditor();
            if (editor != null) {
                IEditorInput editorInput = editor.getEditorInput();
                if (editorInput instanceof FileEditorInput) {
                    IFile f = ((FileEditorInput) editorInput).getFile();
                    if (f != null) {
                        String leafName = f.getFullPath().removeFileExtension().lastSegment();
                        return "*" + leafName; //$NON-NLS-1$
                    }
                }
            }
        }

        return null;
    }

    /**
     * Given a potential non-null root node, this method looks for the currently edited
     * file path and uses it as a key to retrieve the last used item for this file by this
     * selection dialog. Returns null if nothing can be found, otherwise returns the string
     * name of the item.
     */
    private String getLastUsedXmlName(UiElementNode ui_node) {
        if (ui_node != null) {
            AndroidXmlEditor editor = ui_node.getEditor();
            if (editor != null) {
                IEditorInput editorInput = editor.getEditorInput();
                if (editorInput instanceof FileEditorInput) {
                    IFile f = ((FileEditorInput) editorInput).getFile();
                    if (f != null) {
                        mLastUsedKey = f.getFullPath().toPortableString();

                        return sLastUsedXmlName.get(mLastUsedKey);
                    }
                }
            }
        }

        return null;
    }

    /**
     * Sets the last used item for this selection dialog for this file.
     * @param objects The currently selected items. Only the first one is used if it is an
     *                {@link ElementDescriptor}.
     */
    private void setLastUsedXmlName(Object[] objects) {
        if (mLastUsedKey != null &&
                objects != null &&
                objects.length > 0 &&
                objects[0] instanceof ElementDescriptor) {
            ElementDescriptor desc = (ElementDescriptor) objects[0];
            sLastUsedXmlName.put(mLastUsedKey, desc.getXmlName());
        }
    }

    /**
     * Returns the most used sub-element name, if any, or null.
     */
    private String getMostUsedXmlName(UiElementNode ui_node) {
        if (ui_node != null) {
            TreeMap<String, Integer> counts = new TreeMap<String, Integer>();
            int max = -1;

            for (UiElementNode child : ui_node.getUiChildren()) {
                String name = child.getDescriptor().getXmlName();
                Integer i = counts.get(name);
                int count = i == null ? 1 : i.intValue() + 1;
                counts.put(name, count);
                max = Math.max(max, count);
            }

            if (max > 0) {
                // Find first key with this max and return it
                for (Entry<String, Integer> entry : counts.entrySet()) {
                    if (entry.getValue().intValue() == max) {
                        return entry.getKey();
                    }
                }
            }
        }
        return null;
    }

    /**
     * @return The root node selected by the user, either root node or the
     *         one passed to the constructor if not null.
     */
    public UiElementNode getChosenRootNode() {
        return mChosenRootNode;
    }

    /**
     * Internal helper to compute the result. Returns the selection from
     * the list view, if any.
     */
    @Override
    protected void computeResult() {
        setResult(Arrays.asList(getSelectedElements()));
        setLastUsedXmlName(getSelectedElements());
    }

    /**
     * Creates the dialog area.
     *
     * First add a radio area, which may be either 2 radio controls or
     * just a message area if there's only one choice (the app root node).
     *
     * Then uses the default from the AbstractElementListSelectionDialog
     * which is to add both a filter text and a filtered list. Adding both
     * is necessary (since the base class accesses both internal directly
     * fields without checking for null pointers.)
     *
     * Finally sets the initial selection list.
     */
    @Override
    protected Control createDialogArea(Composite parent) {
        Composite contents = (Composite) super.createDialogArea(parent);

        createRadioControl(contents);
        createFilterText(contents);
        createFilteredList(contents);

        // Initialize the list state.
        // This must be done after the filtered list as been created.
        chooseNode(mChosenRootNode);

        // Set the initial selection
        setInitialSelection(mChosenRootNode);
        return contents;
    }

    /**
     * Tries to set the initial selection based on the {@link #mInitialXmlName} computed
     * in the constructor. The selection is only set if there's an element descriptor
     * that matches the same exact XML name. When {@link #mInitialXmlName} starts with an
     * asterisk, it means to do a partial case-insensitive match on the start of the
     * strings.
     */
    private void setInitialSelection(UiElementNode rootNode) {
        ElementDescriptor initialElement = null;

        if (mInitialXmlName != null && mInitialXmlName.length() > 0) {
            String name = mInitialXmlName;
            boolean partial = name.startsWith("*");   //$NON-NLS-1$
            if (partial) {
                name = name.substring(1).toLowerCase();
            }

            for (ElementDescriptor desc : getAllowedDescriptors(rootNode)) {
                if (!partial && desc.getXmlName().equals(name)) {
                    initialElement = desc;
                    break;
                } else if (partial) {
                    String name2 = desc.getXmlLocalName().toLowerCase();
                    if (name.startsWith(name2) || name2.startsWith(name)) {
                        initialElement = desc;
                        break;
                    }
                }
            }
        }

        setSelection(initialElement == null ? null : new ElementDescriptor[] { initialElement });
    }

    /**
     * Creates the message text widget and sets layout data.
     * @param content the parent composite of the message area.
     */
    private Composite createRadioControl(Composite content) {

        if (mSelectedUiNode != null) {
            Button radio1 = new Button(content, SWT.RADIO);
            radio1.setText(String.format("Create a new element at the top level, in %1$s.",
                    mLocalRootNode.getShortDescription()));

            Button radio2 = new Button(content, SWT.RADIO);
            radio2.setText(String.format("Create a new element in the selected element, %1$s.",
                    mSelectedUiNode.getBreadcrumbTrailDescription(false /* include_root */)));

            // Set the initial selection before adding the listeners
            // (they can't be run till the filtered list has been created)
            radio1.setSelection(false);
            radio2.setSelection(true);
            mChosenRootNode = mSelectedUiNode;

            radio1.addSelectionListener(new SelectionAdapter() {
                @Override
                public void widgetSelected(SelectionEvent e) {
                    super.widgetSelected(e);
                    chooseNode(mLocalRootNode);
                }
            });

            radio2.addSelectionListener(new SelectionAdapter() {
                @Override
                public void widgetSelected(SelectionEvent e) {
                    super.widgetSelected(e);
                    chooseNode(mSelectedUiNode);
                }
            });
        } else {
            setMessage(String.format("Create a new element at the top level, in %1$s.",
                    mLocalRootNode.getShortDescription()));
            createMessageArea(content);

            mChosenRootNode = mLocalRootNode;
        }

        return content;
    }

    /**
     * Internal helper to remember the root node choosen by the user.
     * It also sets the list view to the adequate list of children that can
     * be added to the chosen root node.
     *
     * If the chosen root node is mLocalRootNode and a descriptor filter was specified
     * when creating the master-detail part, we use this as the set of nodes that
     * can be created on the root node.
     *
     * @param ui_node The chosen root node, either mLocalRootNode or
     *                mSelectedUiNode.
     */
    private void chooseNode(UiElementNode ui_node) {
        mChosenRootNode = ui_node;
        setListElements(getAllowedDescriptors(ui_node));
    }

    /**
     * Returns the list of {@link ElementDescriptor}s that can be added to the given
     * UI node.
     *
     * @param ui_node The UI node to which element should be added. Cannot be null.
     * @return A non-null array of {@link ElementDescriptor}. The array might be empty.
     */
    private ElementDescriptor[] getAllowedDescriptors(UiElementNode ui_node) {
        if (ui_node == mLocalRootNode &&
                mDescriptorFilters != null &&
                mDescriptorFilters.length != 0) {
            return mDescriptorFilters;
        } else {
            return ui_node.getDescriptor().getChildren();
        }
    }
}
