/*
 * Copyright (C) 2010 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.common.api;

import com.android.annotations.NonNull;
import com.android.annotations.Nullable;
import com.android.utils.Pair;
import com.google.common.annotations.Beta;

import java.net.URL;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

/**
 * A {@link RuleAction} represents an action provided by an {@link IViewRule}, typically
 * shown in a context menu or in the layout actions bar.
 * <p>
 * Each action should have a reasonably unique ID. This is used when multiple nodes
 * are selected to filter the actions down to just those actions that are supported
 * across all selected nodes. If an action does not support multiple nodes, it can
 * return false from {@link #supportsMultipleNodes()}.
 * <p>
 * Actions can be grouped into a hierarchy of sub-menus using the {@link NestedAction} class,
 * or into a flat submenu using the {@link Choices} class.
 * <p>
 * Actions (including separators) all have a "sort priority", and this is used to
 * sort the menu items or toolbar buttons into a specific order.
 * <p>
 * <b>NOTE: This is not a public or final API; if you rely on this be prepared
 * to adjust your code for the next tools release.</b>
 * </p>
 */
@Beta
public class RuleAction implements Comparable<RuleAction> {
    /**
     * Character used to split multiple checked choices.
     * The pipe character "|" is used, to natively match Android resource flag separators.
     */
    public static final String CHOICE_SEP = "|"; //$NON-NLS-1$

    /**
     * Same as {@link #CHOICE_SEP} but safe for use in regular expressions.
     */
    public static final String CHOICE_SEP_PATTERN = Pattern.quote(CHOICE_SEP);

    /**
     * The unique id of the action.
     * @see #getId()
     */
    private final String mId;
    /**
     * The UI-visible title of the action.
     */
    private final String mTitle;

    /** A URL pointing to an icon, or null */
    private URL mIconUrl;

    /**
     * A callback executed when the action is selected in the context menu.
     */
    private final IMenuCallback mCallback;

    /**
     * The sorting priority of this item; actions can be sorted according to these
     */
    protected final int mSortPriority;

    /**
     * Whether this action supports multiple nodes, see
     * {@link #supportsMultipleNodes()} for details.
     */
    private final boolean mSupportsMultipleNodes;

    /**
     * Special value which will insert a separator in the choices' submenu.
     */
    public static final String SEPARATOR = "----";

    // Factories

    /**
     * Constructs a new separator which will be shown in places where separators
     * are supported such as context menus
     *
     * @param sortPriority a priority used for sorting this action
     * @return a new separator
     */
    @NonNull
    public static Separator createSeparator(int sortPriority) {
        return new Separator(sortPriority, true /* supportsMultipleNodes*/);
    }

    /**
     * Constructs a new base {@link RuleAction} with its ID, title and action callback.
     *
     * @param id The unique ID of the action. Must not be null.
     * @param title The title of the action. Must not be null.
     * @param callback The callback executed when the action is selected.
     *            Must not be null.
     * @param iconUrl a URL pointing to an icon to use for this action, or null
     * @param sortPriority a priority used for sorting this action
     * @param supportsMultipleNodes whether this action supports multiple nodes,
     *            see {@link #supportsMultipleNodes()} for details
     * @return the new {@link RuleAction}
     */
    @NonNull
    public static RuleAction createAction(
            @NonNull String id,
            @NonNull String title,
            @NonNull IMenuCallback callback,
            @Nullable URL iconUrl,
            int sortPriority,
            boolean supportsMultipleNodes) {
        RuleAction action = new RuleAction(id, title, callback, sortPriority,
                supportsMultipleNodes);
        action.setIconUrl(iconUrl);

        return action;
    }

    /**
     * Creates a new immutable toggle action.
     *
     * @param id The unique id of the action. Cannot be null.
     * @param title The UI-visible title of the context menu item. Cannot be null.
     * @param isChecked Whether the context menu item has a check mark.
     * @param callback A callback to execute when the context menu item is
     *            selected.
     * @param iconUrl a URL pointing to an icon to use for this action, or null
     * @param sortPriority a priority used for sorting this action
     * @param supportsMultipleNodes whether this action supports multiple nodes,
     *            see {@link #supportsMultipleNodes()} for details
     * @return the new {@link Toggle}
     */
    @NonNull
    public static Toggle createToggle(
            @NonNull String id,
            @NonNull String title,
            boolean isChecked,
            @NonNull IMenuCallback callback,
            @Nullable URL iconUrl,
            int sortPriority,
            boolean supportsMultipleNodes) {
        Toggle toggle = new Toggle(id, title, isChecked, callback, sortPriority,
                supportsMultipleNodes);
        toggle.setIconUrl(iconUrl);
        return toggle;
    }

    /**
     * Creates a new immutable multiple-choice action with a defined ordered set
     * of action children.
     *
     * @param id The unique id of the action. Cannot be null.
     * @param title The title of the action to be displayed to the user
     * @param provider Provides the actions to be shown as children of this
     *            action
     * @param callback A callback to execute when the context menu item is
     *            selected.
     * @param iconUrl the icon to use for the multiple choice action itself
     * @param sortPriority the sorting priority to use for the multiple choice
     *            action itself
     * @param supportsMultipleNodes whether this action supports multiple nodes,
     *            see {@link #supportsMultipleNodes()} for details
     * @return the new {@link NestedAction}
     */
    @NonNull
    public static NestedAction createChoices(
            @NonNull String id,
            @NonNull String title,
            @NonNull IMenuCallback callback,
            @Nullable URL iconUrl,
            int sortPriority,
            boolean supportsMultipleNodes,
            @NonNull ActionProvider provider) {
        NestedAction choices = new NestedAction(id, title, provider, callback,
                sortPriority, supportsMultipleNodes);
        choices.setIconUrl(iconUrl);
        return choices;
    }

    /**
     * Creates a new immutable multiple-choice action with a defined ordered set
     * of children.
     *
     * @param id The unique id of the action. Cannot be null.
     * @param title The title of the action to be displayed to the user
     * @param iconUrls The icon urls for the children items (may be null)
     * @param ids The internal ids for the children
     * @param current The id(s) of the current choice(s) that will be check
     *            marked. Can be null. Can be an id not present in the choices
     *            map. There can be more than one id separated by
     *            {@link #CHOICE_SEP}.
     * @param callback A callback to execute when the context menu item is
     *            selected.
     * @param titles The UI-visible titles of the children
     * @param iconUrl the icon to use for the multiple choice action itself
     * @param sortPriority the sorting priority to use for the multiple choice
     *            action itself
     * @param supportsMultipleNodes whether this action supports multiple nodes,
     *            see {@link #supportsMultipleNodes()} for details
     * @return the new {@link Choices}
     */
    @NonNull
    public static Choices createChoices(
            @NonNull String id,
            @NonNull String title,
            @NonNull IMenuCallback callback,
            @NonNull List<String> titles,
            @Nullable List<URL> iconUrls,
            @NonNull List<String> ids,
            @Nullable String current,
            @Nullable URL iconUrl,
            int sortPriority,
            boolean supportsMultipleNodes) {
        Choices choices = new Choices(id, title, callback, titles, iconUrls,
                ids, current, sortPriority, supportsMultipleNodes);
        choices.setIconUrl(iconUrl);

        return choices;
    }

    /**
     * Creates a new immutable multiple-choice action with a defined ordered set
     * of children.
     *
     * @param id The unique id of the action. Cannot be null.
     * @param title The title of the action to be displayed to the user
     * @param iconUrls The icon urls for the children items (may be null)
     * @param current The id(s) of the current choice(s) that will be check
     *            marked. Can be null. Can be an id not present in the choices
     *            map. There can be more than one id separated by
     *            {@link #CHOICE_SEP}.
     * @param callback A callback to execute when the context menu item is
     *            selected.
     * @param iconUrl the icon to use for the multiple choice action itself
     * @param sortPriority the sorting priority to use for the multiple choice
     *            action itself
     * @param supportsMultipleNodes whether this action supports multiple nodes,
     *            see {@link #supportsMultipleNodes()} for details
     * @param idsAndTitles a list of pairs (of ids and titles) to use for the
     *            menu items
     * @return the new {@link Choices}
     */
    @NonNull
    public static Choices createChoices(
            @NonNull String id,
            @NonNull String title,
            @NonNull IMenuCallback callback,
            @Nullable List<URL> iconUrls,
            @Nullable String current,
            @Nullable URL iconUrl,
            int sortPriority,
            boolean supportsMultipleNodes,
            @NonNull List<Pair<String, String>> idsAndTitles) {
        int itemCount = idsAndTitles.size();
        List<String> titles = new ArrayList<String>(itemCount);
        List<String> ids = new ArrayList<String>(itemCount);
        for (Pair<String, String> pair : idsAndTitles) {
            ids.add(pair.getFirst());
            titles.add(pair.getSecond());
        }
        Choices choices = new Choices(id, title, callback, titles, iconUrls,
                ids, current, sortPriority, supportsMultipleNodes);
        choices.setIconUrl(iconUrl);
        return choices;
    }

    /**
     * Creates a new immutable multiple-choice action with lazily computed children.
     *
     * @param id The unique id of the action. Cannot be null.
     * @param title The title of the multiple-choice itself
     * @param callback A callback to execute when the context menu item is
     *            selected.
     * @param provider the provider which provides choices lazily
     * @param current The id(s) of the current choice(s) that will be check
     *            marked. Can be null. Can be an id not present in the choice
     *            alternatives. There can be more than one id separated by
     *            {@link #CHOICE_SEP}.
     * @param iconUrl the icon to use for the multiple choice action itself
     * @param sortPriority the sorting priority to use for the multiple choice
     *            action itself
     * @param supportsMultipleNodes whether this action supports multiple nodes,
     *            see {@link #supportsMultipleNodes()} for details
     * @return the new {@link Choices}
     */
    @NonNull
    public static Choices createChoices(
            @NonNull String id,
            @NonNull String title,
            IMenuCallback callback,
            @NonNull ChoiceProvider provider,
            @Nullable String current,
            @Nullable URL iconUrl,
            int sortPriority,
            boolean supportsMultipleNodes) {
        Choices choices = new DelayedChoices(id, title, callback,
                current, provider, sortPriority, supportsMultipleNodes);
        choices.setIconUrl(iconUrl);
        return choices;
    }

    /**
     * Creates a new {@link RuleAction} with the given id and the given title.
     * Actions which have the same id and the same title are deemed equivalent.
     *
     * @param id The unique id of the action, which must be similar for all actions that
     *           perform the same task. Cannot be null.
     * @param title The UI-visible title of the action.
     * @param callback A callback to execute when the context menu item is
     *            selected.
     * @param sortPriority a priority used for sorting this action
     * @param supportsMultipleNodes the new return value for
     *            {@link #supportsMultipleNodes()}
     */
    private RuleAction(
            @NonNull String id,
            @NonNull String title,
            @NonNull IMenuCallback callback,
            int sortPriority,
            boolean supportsMultipleNodes) {
        mId = id;
        mTitle = title;
        mSortPriority = sortPriority;
        mSupportsMultipleNodes = supportsMultipleNodes;
        mCallback = callback;
    }

    /**
     * Returns the unique id of the action. In the context of a multiple selection,
     * actions which have the same id are collapsed together and must represent the same
     * action. Cannot be null.
     *
     * @return the unique id of the action, never null
     */
    @NonNull
    public String getId() {
        return mId;
    }

    /**
     * Returns the UI-visible title of the action, shown in the context menu.
     * Cannot be null.
     *
     * @return the user name of the action, never null
     */
    @NonNull
    public String getTitle() {
        return mTitle;
    }

    /**
     * Actions which have the same id and the same title are deemed equivalent.
     */
    @Override
    public boolean equals(Object obj) {
        if (obj instanceof RuleAction) {
            RuleAction rhs = (RuleAction) obj;

            if (mId != rhs.mId && !(mId != null && mId.equals(rhs.mId))) return false;
            if (mTitle != rhs.mTitle &&
                    !(mTitle != null && mTitle.equals(rhs.mTitle))) return false;
            return true;
        }
        return false;
    }

    /**
     * Whether this action supports multiple nodes. An action which supports
     * multiple nodes can be applied to different nodes by passing in different
     * nodes to its callback. Some actions are hardcoded for a specific node (typically
     * one that isn't selected, such as an action which affects the parent of a selected
     * node), and these actions will not be added to the context menu when more than
     * one node is selected.
     *
     * @return true if this node supports multiple nodes
     */
    public boolean supportsMultipleNodes() {
        return mSupportsMultipleNodes;
    }

    /**
     * Actions which have the same id and the same title have the same hash code.
     */
    @Override
    public int hashCode() {
        int h = mId == null ? 0 : mId.hashCode();
        h = h ^ (mTitle == null ? 0 : mTitle.hashCode());
        return h;
    }

    /**
     * Gets a URL pointing to an icon to use for this action, if any.
     *
     * @return a URL pointing to an icon to use for this action, or null
     */
    public URL getIconUrl() {
        return mIconUrl;
    }

    /**
     * Sets a URL pointing to an icon to use for this action, if any.
     *
     * @param iconUrl a URL pointing to an icon to use for this action, or null
     * @return this action, to allow setter chaining
     */
    @NonNull
    public RuleAction setIconUrl(URL iconUrl) {
        mIconUrl = iconUrl;

        return this;
    }

    /**
     * Return a priority used for sorting this action
     *
     * @return a priority used for sorting this action
     */
    public int getSortPriority() {
        return mSortPriority;
    }

    /**
     * Returns the callback executed when the action is selected in the
     * context menu. Cannot be null.
     *
     * @return the callback, never null
     */
    @NonNull
    public IMenuCallback getCallback() {
        return mCallback;
    }

    // Implements Comparable<MenuAction>
    @Override
    public int compareTo(RuleAction other) {
        if (mSortPriority != other.mSortPriority) {
            return mSortPriority - other.mSortPriority;
        }

        return mTitle.compareTo(other.mTitle);
    }

    @NonNull
    @Override
    public String toString() {
        return "RuleAction [id=" + mId + ", title=" + mTitle + ", priority=" + mSortPriority + "]";
    }

    /** A separator to display between actions */
    public static class Separator extends RuleAction {
        /** Construct using the factory {@link #createSeparator(int)} */
        private Separator(int sortPriority, boolean supportsMultipleNodes) {
            super("_separator", "", IMenuCallback.NONE, sortPriority,  //$NON-NLS-1$ //$NON-NLS-2$
                    supportsMultipleNodes);
        }
    }

    /**
     * A toggle is a simple on/off action, displayed as an item in a context menu
     * with a check mark if the item is checked.
     * <p>
     * Two toggles are equal if they have the same id, title and group-id.
     * It is expected for the checked state and action callback to be different.
     */
    public static class Toggle extends RuleAction {
        /**
         * True if the item is displayed with a check mark.
         */
        private final boolean mIsChecked;

        /**
         * Creates a new immutable toggle action.
         *
         * @param id The unique id of the action. Cannot be null.
         * @param title The UI-visible title of the context menu item. Cannot be null.
         * @param isChecked Whether the context menu item has a check mark.
         * @param callback A callback to execute when the context menu item is
         *            selected.
         */
        private Toggle(
                @NonNull String id,
                @NonNull String title,
                boolean isChecked,
                @NonNull IMenuCallback callback,
                int sortPriority,
                boolean supportsMultipleNodes) {
            super(id, title, callback, sortPriority, supportsMultipleNodes);
            mIsChecked = isChecked;
        }

        /**
         * Returns true if the item is displayed with a check mark.
         *
         * @return true if the item is displayed with a check mark.
         */
        public boolean isChecked() {
            return mIsChecked;
        }

        /**
         * Two toggles are equal if they have the same id and title.
         * It is acceptable for the checked state and action callback to be different.
         */
        @Override
        public boolean equals(Object obj) {
            return super.equals(obj);
        }

        /**
         * Two toggles have the same hash code if they have the same id and title.
         */
        @Override
        public int hashCode() {
            return super.hashCode();
        }
    }

    /**
     * An ordered list of choices the user can choose between. For choosing between
     * actions, there is a {@link NestedAction} class.
     */
    public static class Choices extends RuleAction {
        protected List<String> mTitles;
        protected List<URL> mIconUrls;
        protected List<String> mIds;
        private boolean mRadio;

        /**
         * One or more id for the checked choice(s) that will be check marked.
         * Can be null. Can be an id not present in the choices map.
         */
        protected final String mCurrent;

        private Choices(
                @NonNull String id,
                @NonNull String title,
                @NonNull IMenuCallback callback,
                @NonNull List<String> titles,
                @Nullable List<URL> iconUrls,
                @NonNull List<String> ids,
                @Nullable String current,
                int sortPriority,
                boolean supportsMultipleNodes) {
            super(id, title, callback, sortPriority, supportsMultipleNodes);
            mTitles = titles;
            mIconUrls = iconUrls;
            mIds = ids;
            mCurrent = current;
        }

        /**
         * Returns the list of urls to icons to display for each choice, or null
         *
         * @return the list of urls to icons to display for each choice, or null
         */
        @Nullable
        public List<URL> getIconUrls() {
            return mIconUrls;
        }

        /**
         * Returns the list of ids for the menu choices, never null
         *
         * @return the list of ids for the menu choices, never null
         */
        @NonNull
        public List<String> getIds() {
            return mIds;
        }

        /**
         * Returns the titles to be displayed for the menu choices, never null
         *
         * @return the titles to be displayed for the menu choices, never null
         */
        @NonNull
        public List<String> getTitles() {
            return mTitles;
        }

        /**
         * Returns the current value of the choice
         *
         * @return the current value of the choice, possibly null
         */
        @Nullable
        public String getCurrent() {
            return mCurrent;
        }

        /**
         * Set whether this choice list is best visualized as a radio group (instead of a
         * dropdown)
         *
         * @param radio true if this choice list should be visualized as a radio group
         */
        public void setRadio(boolean radio) {
            mRadio = radio;
        }

        /**
         * Returns true if this choice list is best visualized as a radio group (instead
         * of a dropdown)
         *
         * @return true if this choice list should be visualized as a radio group
         */
        public boolean isRadio() {
            return mRadio;
        }
    }

    /**
     * An ordered list of actions the user can choose between. Similar to
     * {@link Choices} but for actions instead.
     */
    public static class NestedAction extends RuleAction {
        /** The provider to produce the list of nested actions when needed */
        private final ActionProvider mProvider;

        private NestedAction(
                @NonNull String id,
                @NonNull String title,
                @NonNull ActionProvider provider,
                @NonNull IMenuCallback callback,
                int sortPriority,
                boolean supportsMultipleNodes) {
            super(id, title, callback, sortPriority, supportsMultipleNodes);
            mProvider = provider;
        }

        /**
         * Returns the nested actions available for the given node
         *
         * @param node the node to look up nested actions for
         * @return a list of nested actions
         */
        @NonNull
        public List<RuleAction> getNestedActions(@NonNull INode node) {
            return mProvider.getNestedActions(node);
        }
    }

    /** Like {@link Choices}, but the set of choices is computed lazily */
    private static class DelayedChoices extends Choices {
        private final ChoiceProvider mProvider;
        private boolean mInitialized;

        private DelayedChoices(
                @NonNull String id,
                @NonNull String title,
                @NonNull IMenuCallback callback,
                @Nullable String current,
                @NonNull ChoiceProvider provider,
                int sortPriority, boolean supportsMultipleNodes) {
            super(id, title, callback, new ArrayList<String>(), new ArrayList<URL>(),
                    new ArrayList<String>(), current, sortPriority, supportsMultipleNodes);
            mProvider = provider;
        }

        private void ensureInitialized() {
            if (!mInitialized) {
                mInitialized = true;
                mProvider.addChoices(mTitles, mIconUrls, mIds);
            }
        }

        @Override
        public List<URL> getIconUrls() {
            ensureInitialized();
            return mIconUrls;
        }

        @NonNull
        @Override
        public List<String> getIds() {
            ensureInitialized();
            return mIds;
        }

        @NonNull
        @Override
        public List<String> getTitles() {
            ensureInitialized();
            return mTitles;
        }
    }

    /**
     * Provides the set of nested action choices associated with a {@link NestedAction}
     * object when they are needed. Useful for lazy initialization of context
     * menus and popup menus until they are actually needed.
     */
    public interface ActionProvider {
        /**
         * Returns the nested actions available for the given node
         *
         * @param node the node to look up nested actions for
         * @return a list of nested actions
         */
        @NonNull
        List<RuleAction> getNestedActions(@NonNull INode node);
    }

    /**
     * Provides the set of choices associated with an {@link Choices}
     * object when they are needed. Useful for lazy initialization of context
     * menus and popup menus until they are actually needed.
     */
    public interface ChoiceProvider {
        /**
         * Adds in the needed titles, iconUrls (if any) and ids.
         * Use {@link RuleAction#SEPARATOR} to create separators.
         *
         * @param titles a list of titles that the provider should append to
         * @param iconUrls a list of icon URLs that the provider should append to
         * @param ids a list of ids that the provider should append to
         */
        void addChoices(
                @NonNull List<String> titles,
                @NonNull List<URL> iconUrls,
                @NonNull List<String> ids);
    }
}
