/*
 * Copyright (C) 2011 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.layout.gle2;

import com.android.ide.eclipse.adt.internal.editors.IconFactory;

import org.eclipse.swt.SWT;
import org.eclipse.swt.custom.CLabel;
import org.eclipse.swt.custom.ScrolledComposite;
import org.eclipse.swt.events.ControlAdapter;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.MouseAdapter;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseTrackListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Font;
import org.eclipse.swt.graphics.FontData;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.layout.RowLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.ScrollBar;

import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

/**
 * The accordion control allows a series of labels with associated content that can be
 * shown. For more details on accordions, see http://en.wikipedia.org/wiki/Accordion_(GUI)
 * <p>
 * This control allows the children to be created lazily. You can also customize the
 * composite which is created to hold the children items, to for example allow multiple
 * columns of items rather than just the default vertical stack.
 * <p>
 * The visual appearance of the headers is built in; it uses a mild gradient, with a
 * heavier gradient during mouse-overs. It also uses a bold label along with the eclipse
 * folder icons.
 * <p>
 * The control can be configured to enforce a single category open at any time (the
 * default), or allowing multiple categories open (where they share the available space).
 * The control can also be configured to fill the available vertical space for the open
 * category/categories.
 */
public abstract class AccordionControl extends Composite {
    /** Pixel spacing between header items */
    private static final int HEADER_SPACING = 0;

    /** Pixel spacing between items in the content area */
    private static final int ITEM_SPACING = 0;

    private static final String KEY_CONTENT = "content"; //$NON-NLS-1$
    private static final String KEY_HEADER = "header"; //$NON-NLS-1$

    private Image mClosed;
    private Image mOpen;
    private boolean mSingle = true;
    private boolean mWrap;

    /**
     * Creates the container which will hold the items in a category; this can be
     * overridden to lay out the children with a different layout than the default
     * vertical RowLayout
     */
    protected Composite createChildContainer(Composite parent, Object header, int style) {
        Composite composite = new Composite(parent, style);
        if (mWrap) {
            RowLayout layout = new RowLayout(SWT.HORIZONTAL);
            layout.center = true;
            composite.setLayout(layout);
        } else {
            RowLayout layout = new RowLayout(SWT.VERTICAL);
            layout.spacing = ITEM_SPACING;
            layout.marginHeight = 0;
            layout.marginWidth = 0;
            layout.marginLeft = 0;
            layout.marginTop = 0;
            layout.marginRight = 0;
            layout.marginBottom = 0;
            composite.setLayout(layout);
        }

        // TODO - maybe do multi-column arrangement for simple nodes
        return composite;
    }

    /**
     * Creates the children under a particular header
     *
     * @param parent the parent composite to add the SWT items to
     * @param header the header object that is being opened for the first time
     */
    protected abstract void createChildren(Composite parent, Object header);

    /**
     * Set whether a single category should be enforced or not (default=true)
     *
     * @param single if true, enforce a single category open at a time
     */
    public void setAutoClose(boolean single) {
        mSingle = single;
    }

    /**
     * Returns whether a single category should be enforced or not (default=true)
     *
     * @return true if only a single category can be open at a time
     */
    public boolean isAutoClose() {
        return mSingle;
    }

    /**
     * Returns the labels used as header categories
     *
     * @return list of header labels
     */
    public List<CLabel> getHeaderLabels() {
        List<CLabel> headers = new ArrayList<CLabel>();
        for (Control c : getChildren()) {
            if (c instanceof CLabel) {
                headers.add((CLabel) c);
            }
        }

        return headers;
    }

    /**
     * Show all categories
     *
     * @param performLayout if true, call {@link #layout} and {@link #pack} when done
     */
    public void expandAll(boolean performLayout) {
        for (Control c : getChildren()) {
            if (c instanceof CLabel) {
                if (!isOpen(c)) {
                    toggle((CLabel) c, false, false);
                }
            }
        }
        if (performLayout) {
            pack();
            layout();
        }
    }

    /**
     * Hide all categories
     *
     * @param performLayout if true, call {@link #layout} and {@link #pack} when done
     */
    public void collapseAll(boolean performLayout) {
        for (Control c : getChildren()) {
            if (c instanceof CLabel) {
                if (isOpen(c)) {
                    toggle((CLabel) c, false, false);
                }
            }
        }
        if (performLayout) {
            layout();
        }
    }

    /**
     * Create the composite.
     *
     * @param parent the parent widget to add the accordion to
     * @param style the SWT style mask to use
     * @param headers a list of headers, whose {@link Object#toString} method should
     *            produce the heading label
     * @param greedy if true, grow vertically as much as possible
     * @param wrapChildren if true, configure the child area to be horizontally laid out
     *            with wrapping
     * @param expand Set of headers to expand initially
     */
    public AccordionControl(Composite parent, int style, List<?> headers,
            boolean greedy, boolean wrapChildren, Set<String> expand) {
        super(parent, style);
        mWrap = wrapChildren;

        GridLayout gridLayout = new GridLayout(1, false);
        gridLayout.verticalSpacing = HEADER_SPACING;
        gridLayout.horizontalSpacing = 0;
        gridLayout.marginWidth = 0;
        gridLayout.marginHeight = 0;
        setLayout(gridLayout);

        Font labelFont = null;

        mOpen = IconFactory.getInstance().getIcon("open-folder");     //$NON-NLS-1$
        mClosed = IconFactory.getInstance().getIcon("closed-folder"); //$NON-NLS-1$
        List<CLabel> expandLabels = new ArrayList<CLabel>();

        for (Object header : headers) {
            final CLabel label = new CLabel(this, SWT.SHADOW_OUT);
            label.setText(header.toString().replace("&", "&&")); //$NON-NLS-1$ //$NON-NLS-2$
            updateBackground(label, false);
            if (labelFont == null) {
                labelFont = label.getFont();
                FontData normal = labelFont.getFontData()[0];
                FontData bold = new FontData(normal.getName(), normal.getHeight(), SWT.BOLD);
                labelFont = new Font(null, bold);
            }
            label.setFont(labelFont);
            label.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
            setHeader(header, label);
            label.addMouseListener(new MouseAdapter() {
                @Override
                public void mouseUp(MouseEvent e) {
                    if (e.button == 1 && (e.stateMask & SWT.MODIFIER_MASK) == 0) {
                        toggle(label, true, mSingle);
                    }
                }
            });
            label.addMouseTrackListener(new MouseTrackListener() {
                public void mouseEnter(MouseEvent e) {
                    updateBackground(label, true);
                }

                public void mouseExit(MouseEvent e) {
                    updateBackground(label, false);
                }

                public void mouseHover(MouseEvent e) {
                }
            });

            // Turn off border?
            final ScrolledComposite scrolledComposite = new ScrolledComposite(this, SWT.V_SCROLL);
            ScrollBar verticalBar = scrolledComposite.getVerticalBar();
            verticalBar.setIncrement(20);
            verticalBar.setPageIncrement(100);

            // Do we need the scrolled composite or can we just look at the next
            // wizard in the hierarchy?

            setContentArea(label, scrolledComposite);
            scrolledComposite.setExpandHorizontal(true);
            scrolledComposite.setExpandVertical(true);
            GridData scrollGridData = new GridData(SWT.FILL,
                    greedy ? SWT.FILL : SWT.TOP, false, greedy, 1, 1);
            scrollGridData.exclude = true;
            scrollGridData.grabExcessHorizontalSpace = wrapChildren;
            scrolledComposite.setLayoutData(scrollGridData);

            if (wrapChildren) {
                scrolledComposite.addControlListener(new ControlAdapter() {
                    @Override
                    public void controlResized(ControlEvent e) {
                        Rectangle r = scrolledComposite.getClientArea();
                        Control content = scrolledComposite.getContent();
                        if (content != null && r != null) {
                            Point minSize = content.computeSize(r.width, SWT.DEFAULT);
                            scrolledComposite.setMinSize(minSize);
                            ScrollBar vBar = scrolledComposite.getVerticalBar();
                            vBar.setPageIncrement(r.height);
                        }
                    }
                  });
            }

            updateIcon(label);
            if (expand != null && expand.contains(label.getText())) {
                // Comparing "label.getText()" rather than "header" because we make some
                // tweaks to the label (replacing & with && etc) and in the getExpandedCategories
                // method we return the label texts
                expandLabels.add(label);
            }
        }

        // Expand the requested categories
        for (CLabel label : expandLabels) {
            toggle(label, false, false);
        }
    }

    /** Updates the background gradient of the given header label */
    private void updateBackground(CLabel label, boolean mouseOver) {
        Display display = label.getDisplay();
        label.setBackground(new Color[] {
                display.getSystemColor(SWT.COLOR_WIDGET_HIGHLIGHT_SHADOW),
                display.getSystemColor(SWT.COLOR_WIDGET_BACKGROUND),
                display.getSystemColor(SWT.COLOR_WIDGET_LIGHT_SHADOW)
        }, new int[] {
                mouseOver ? 60 : 40, 100
        }, true);
    }

    /**
     * Updates the icon for a header label to be open/close based on the {@link #isOpen}
     * state
     */
    private void updateIcon(CLabel label) {
        label.setImage(isOpen(label) ? mOpen : mClosed);
    }

    /** Returns true if the content area for the given label is open/showing */
    private boolean isOpen(Control label) {
        return !((GridData) getContentArea(label).getLayoutData()).exclude;
    }

    /** Toggles the visibility of the children of the given label */
    private void toggle(CLabel label, boolean performLayout, boolean autoClose) {
        if (autoClose) {
            collapseAll(true);
        }
        ScrolledComposite scrolledComposite = getContentArea(label);

        GridData scrollGridData = (GridData) scrolledComposite.getLayoutData();
        boolean close = !scrollGridData.exclude;
        scrollGridData.exclude = close;
        scrolledComposite.setVisible(!close);
        updateIcon(label);

        if (!scrollGridData.exclude && scrolledComposite.getContent() == null) {
            Object header = getHeader(label);
            Composite composite = createChildContainer(scrolledComposite, header, SWT.NONE);
            createChildren(composite, header);
            while (composite.getParent() != scrolledComposite) {
                composite = composite.getParent();
            }
            scrolledComposite.setContent(composite);
            scrolledComposite.setMinSize(composite.computeSize(SWT.DEFAULT, SWT.DEFAULT));
        }

        if (performLayout) {
            layout(true);
        }
    }

    /** Returns the header object for the given header label */
    private Object getHeader(Control label) {
        return label.getData(KEY_HEADER);
    }

    /** Sets the header object for the given header label */
    private void setHeader(Object header, final CLabel label) {
        label.setData(KEY_HEADER, header);
    }

    /** Returns the content area for the given header label */
    private ScrolledComposite getContentArea(Control label) {
        return (ScrolledComposite) label.getData(KEY_CONTENT);
    }

    /** Sets the content area for the given header label */
    private void setContentArea(final CLabel label, ScrolledComposite scrolledComposite) {
        label.setData(KEY_CONTENT, scrolledComposite);
    }

    @Override
    protected void checkSubclass() {
        // Disable the check that prevents subclassing of SWT components
    }

    /**
     * Returns the set of expanded categories in the palette. Note: Header labels will have
     * escaped ampersand characters with double ampersands.
     *
     * @return the set of expanded categories in the palette - never null
     */
    public Set<String> getExpandedCategories() {
        Set<String> expanded = new HashSet<String>();
        for (Control c : getChildren()) {
            if (c instanceof CLabel) {
                if (isOpen(c)) {
                    expanded.add(((CLabel) c).getText());
                }
            }
        }

        return expanded;
    }
}
