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

import static org.eclipse.jface.viewers.StyledString.DECORATIONS_STYLER;
import static org.eclipse.jface.viewers.StyledString.QUALIFIER_STYLER;

import com.android.ide.eclipse.adt.internal.editors.layout.LayoutEditor;
import com.android.ide.eclipse.adt.internal.resources.ResourceNameValidator;
import com.android.resources.ResourceType;
import com.android.util.Pair;

import org.eclipse.core.resources.IProject;
import org.eclipse.jface.viewers.CheckStateChangedEvent;
import org.eclipse.jface.viewers.CheckboxTableViewer;
import org.eclipse.jface.viewers.ICheckStateListener;
import org.eclipse.jface.viewers.IStructuredContentProvider;
import org.eclipse.jface.viewers.StyledCellLabelProvider;
import org.eclipse.jface.viewers.StyledString;
import org.eclipse.jface.viewers.Viewer;
import org.eclipse.jface.viewers.ViewerCell;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.layout.RowLayout;
import org.eclipse.swt.widgets.Button;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.Text;
import org.w3c.dom.Attr;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;

class ExtractStyleWizard extends VisualRefactoringWizard {
    public ExtractStyleWizard(ExtractStyleRefactoring ref, LayoutEditor editor) {
        super(ref, editor);
        setDefaultPageTitle(ref.getName());
    }

    @Override
    protected void addUserInputPages() {
        String initialName = "styleName";
        addPage(new InputPage(mEditor.getProject(), initialName));
    }

    /**
     * Wizard page which inputs parameters for the {@link ExtractStyleRefactoring}
     * operation
     */
    private static class InputPage extends VisualRefactoringInputPage {
        private final IProject mProject;
        private final String mSuggestedName;
        private Text mNameText;
        private Table mTable;
        private Button mRemoveExtracted;
        private Button mSetStyle;
        private Button mRemoveAll;
        private Button mExtend;;
        private CheckboxTableViewer mCheckedView;

        private String mParentStyle;
        private Set<Attr> mInSelection;
        private List<Attr> mAllAttributes;
        private int mElementCount;
        private Map<Attr, Integer> mFrequencyCount;
        private Set<Attr> mShown;
        private List<Attr> mInitialChecked;
        private List<Map.Entry<String, List<Attr>>> mRoot;
        private Map<String, List<Attr>> mAvailableAttributes;

        public InputPage(IProject project, String suggestedName) {
            super("ExtractStyleInputPage");
            mProject = project;
            mSuggestedName = suggestedName;
        }

        public void createControl(Composite parent) {
            initialize();

            Composite composite = new Composite(parent, SWT.NONE);
            composite.setLayout(new GridLayout(2, false));

            Label nameLabel = new Label(composite, SWT.NONE);
            nameLabel.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false, 1, 1));
            nameLabel.setText("Style Name:");

            mNameText = new Text(composite, SWT.BORDER);
            mNameText.setLayoutData(new GridData(SWT.FILL, SWT.CENTER, true, false, 1, 1));
            mNameText.addModifyListener(mModifyValidateListener);

            mRemoveExtracted = new Button(composite, SWT.CHECK);
            mRemoveExtracted.setSelection(true);
            mRemoveExtracted.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false, 2, 1));
            mRemoveExtracted.setText("Remove extracted attributes");
            mRemoveExtracted.addSelectionListener(mSelectionValidateListener);

            mRemoveAll = new Button(composite, SWT.CHECK);
            mRemoveAll.setSelection(false);
            mRemoveAll.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false, 2, 1));
            mRemoveAll.setText("Remove all extracted attributes regardless of value");
            mRemoveAll.addSelectionListener(mSelectionValidateListener);

            boolean defaultSetStyle = false;
            if (mParentStyle != null) {
                mExtend = new Button(composite, SWT.CHECK);
                mExtend.setSelection(true);
                mExtend.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false, 2, 1));
                mExtend.setText(String.format("Extend %1$s", mParentStyle));
                mExtend.addSelectionListener(mSelectionValidateListener);
                defaultSetStyle = true;
            }

            mSetStyle = new Button(composite, SWT.CHECK);
            mSetStyle.setSelection(defaultSetStyle);
            mSetStyle.setLayoutData(new GridData(SWT.LEFT, SWT.TOP, false, false, 2, 1));
            mSetStyle.setText("Set style attribute on extracted elements");
            mSetStyle.addSelectionListener(mSelectionValidateListener);

            new Label(composite, SWT.NONE);
            new Label(composite, SWT.NONE);

            Label tableLabel = new Label(composite, SWT.NONE);
            tableLabel.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 2, 1));
            tableLabel.setText("Choose style attributes to extract:");

            mCheckedView = CheckboxTableViewer.newCheckList(composite, SWT.BORDER
                    | SWT.FULL_SELECTION | SWT.HIDE_SELECTION);
            mTable = mCheckedView.getTable();
            mTable.setLayoutData(new GridData(SWT.FILL, SWT.FILL, true, true, 2, 2));
            ((GridData) mTable.getLayoutData()).heightHint = 200;

            mCheckedView.setContentProvider(new ArgumentContentProvider());
            mCheckedView.setLabelProvider(new ArgumentLabelProvider());
            mCheckedView.setInput(mRoot);
            final Object[] initialSelection = mInitialChecked.toArray();
            mCheckedView.setCheckedElements(initialSelection);

            mCheckedView.addCheckStateListener(new ICheckStateListener() {
                public void checkStateChanged(CheckStateChangedEvent event) {
                    // Try to disable other elements that conflict with this
                    boolean isChecked = event.getChecked();
                    if (isChecked) {
                        Attr attribute = (Attr) event.getElement();
                        List<Attr> list = mAvailableAttributes.get(attribute.getLocalName());
                        for (Attr other : list) {
                            if (other != attribute && mShown.contains(other)) {
                                mCheckedView.setChecked(other, false);
                            }
                        }
                    }

                    validatePage();
                }
            });

            // Select All / Deselect All
            Composite buttonForm = new Composite(composite, SWT.NONE);
            buttonForm.setLayoutData(new GridData(SWT.LEFT, SWT.CENTER, false, false, 2, 1));
            RowLayout rowLayout = new RowLayout(SWT.HORIZONTAL);
            rowLayout.marginTop = 0;
            rowLayout.marginLeft = 0;
            buttonForm.setLayout(rowLayout);
            Button checkAllButton = new Button(buttonForm, SWT.FLAT);
            checkAllButton.setText("Select All");
            checkAllButton.addSelectionListener(new SelectionAdapter() {
                @Override
                public void widgetSelected(SelectionEvent e) {
                    // Select "all" (but not conflicting settings)
                    mCheckedView.setCheckedElements(initialSelection);
                }
            });
            Button uncheckAllButton = new Button(buttonForm, SWT.FLAT);
            uncheckAllButton.setText("Deselect All");
            uncheckAllButton.addSelectionListener(new SelectionAdapter() {
                @Override
                public void widgetSelected(SelectionEvent e) {
                    mCheckedView.setAllChecked(false);
                }
            });

            // Initialize UI:
            if (mSuggestedName != null) {
                mNameText.setText(mSuggestedName);
            }

            setControl(composite);
            validatePage();
        }

        private void initialize() {
            ExtractStyleRefactoring ref = (ExtractStyleRefactoring) getRefactoring();

            mElementCount = ref.getElements().size();

            mParentStyle = ref.getParentStyle();

            // Set up data structures needed by the wizard -- to compute the actual
            // attributes to list in the wizard (there could be multiple attributes
            // of the same name (on different elements) and we only want to show one, etc.)

            Pair<Map<String, List<Attr>>, Set<Attr>> result = ref.getAvailableAttributes();
            // List of all available attributes on the selected elements
            mAvailableAttributes = result.getFirst();
            // Set of attributes that overlap the text selection, or all attributes if
            // wizard is invoked from GUI context
            mInSelection = result.getSecond();

            // The root data structure, which we set as the table root. The content provider
            // will produce children from it. This is the entry set of a map from
            // attribute name to list of attribute nodes for that attribute name.
            mRoot = new ArrayList<Map.Entry<String, List<Attr>>>(
                mAvailableAttributes.entrySet());

            // Sort the items by attribute name -- the attribute name is the key
            // in the entry set above.
            Collections.sort(mRoot, new Comparator<Map.Entry<String, List<Attr>>>() {
                public int compare(Map.Entry<String, List<Attr>> e1,
                        Map.Entry<String, List<Attr>> e2) {
                    return e1.getKey().compareTo(e2.getKey());
                }
            });

            // Set of attributes actually included in the list shown to the user.
            // (There could be many additional "aliasing" nodes on other elements
            // with the same name.) Note however that we DO show multiple attribute
            // occurrences of the same attribute name: precisely one for each unique -value-
            // of that attribute.
            mShown = new HashSet<Attr>();

            // The list of initially checked attributes.
            mInitialChecked = new ArrayList<Attr>();

            // All attributes.
            mAllAttributes = new ArrayList<Attr>();

            // Frequency count, from attribute to integer. Attributes that do not
            // appear in the list have frequency 1, not 0.
            mFrequencyCount = new HashMap<Attr, Integer>();

            for (Map.Entry<String, List<Attr>> entry : mRoot) {
                // Iterate over all attributes of the same name, and sort them
                // by value. This will make it easy to list each -unique- value in the
                // wizard.
                List<Attr> attrList = entry.getValue();
                Collections.sort(attrList, new Comparator<Attr>() {
                    public int compare(Attr a1, Attr a2) {
                        return a1.getValue().compareTo(a2.getValue());
                    }
                });

                // We need to compute a couple of things: the frequency for all identical
                // values (and stash them in the frequency map), and record the first
                // attribute with a particular value into the list of attributes to
                // be shown.
                Attr prevAttr = null;
                String prev = null;
                List<Attr> uniqueValueAttrs = new ArrayList<Attr>();
                for (Attr attr : attrList) {
                    String value = attr.getValue();
                    if (value.equals(prev)) {
                        Integer count = mFrequencyCount.get(prevAttr);
                        if (count == null) {
                            count = Integer.valueOf(2);
                        } else {
                            count = Integer.valueOf(count.intValue() + 1);
                        }
                        mFrequencyCount.put(prevAttr, count);
                    } else {
                        uniqueValueAttrs.add(attr);
                        prev = value;
                        prevAttr = attr;
                    }
                }

                // Sort the values by frequency (and for equal frequencies, alphabetically
                // by value)
                Collections.sort(uniqueValueAttrs, new Comparator<Attr>() {
                    public int compare(Attr a1, Attr a2) {
                        Integer f1 = mFrequencyCount.get(a1);
                        Integer f2 = mFrequencyCount.get(a2);
                        if (f1 == null) {
                            f1 = Integer.valueOf(1);
                        }
                        if (f2 == null) {
                            f2 = Integer.valueOf(1);
                        }
                        int delta = f2.intValue() - f1.intValue();
                        if (delta != 0) {
                            return delta;
                        } else {
                            return a1.getValue().compareTo(a2.getValue());
                        }
                    }
                });

                // Add the items in order, and select those attributes that overlap
                // the selection
                mAllAttributes.addAll(uniqueValueAttrs);
                mShown.addAll(uniqueValueAttrs);
                Attr first = uniqueValueAttrs.get(0);
                if (mInSelection.contains(first)) {
                    mInitialChecked.add(first);
                }
            }
        }

        @Override
        protected boolean validatePage() {
            boolean ok = true;

            String text = mNameText.getText().trim();

            if (text.length() == 0) {
                setErrorMessage("Provide a name for the new style");
                ok = false;
            } else {
                ResourceNameValidator validator = ResourceNameValidator.create(false, mProject,
                        ResourceType.STYLE);
                String message = validator.isValid(text);
                if (message != null) {
                    setErrorMessage(message);
                    ok = false;
                }
            }

            Object[] checkedElements = mCheckedView.getCheckedElements();
            if (checkedElements.length == 0) {
                setErrorMessage("Choose at least one attribute to extract");
                ok = false;
            }

            if (ok) {
                setErrorMessage(null);

                // Record state
                ExtractStyleRefactoring refactoring = (ExtractStyleRefactoring) getRefactoring();
                refactoring.setStyleName(text);
                refactoring.setRemoveExtracted(mRemoveExtracted.getSelection());
                refactoring.setRemoveAll(mRemoveAll.getSelection());
                refactoring.setApplyStyle(mSetStyle.getSelection());
                if (mExtend != null && mExtend.getSelection()) {
                    refactoring.setParent(mParentStyle);
                }
                List<Attr> attributes = new ArrayList<Attr>();
                for (Object o : checkedElements) {
                    attributes.add((Attr) o);
                }
                refactoring.setChosenAttributes(attributes);
            }

            setPageComplete(ok);
            return ok;
        }

        private class ArgumentLabelProvider extends StyledCellLabelProvider {
            public ArgumentLabelProvider() {
            }

            @Override
            public void update(ViewerCell cell) {
                Object element = cell.getElement();
                Attr attribute = (Attr) element;

                StyledString styledString = new StyledString();
                styledString.append(attribute.getLocalName());
                styledString.append(" = ", QUALIFIER_STYLER);
                styledString.append(attribute.getValue());

                if (mElementCount > 1) {
                    Integer f = mFrequencyCount.get(attribute);
                    String s = String.format(" (in %d/%d elements)",
                            f != null ? f.intValue(): 1, mElementCount);
                    styledString.append(s, DECORATIONS_STYLER);
                }
                cell.setText(styledString.toString());
                cell.setStyleRanges(styledString.getStyleRanges());
                super.update(cell);
            }
        }

        private class ArgumentContentProvider implements IStructuredContentProvider {
            public ArgumentContentProvider() {
            }

            public Object[] getElements(Object inputElement) {
                if (inputElement == mRoot) {
                    return mAllAttributes.toArray();
                }

                return new Object[0];
            }

            public void dispose() {
            }

            public void inputChanged(Viewer viewer, Object oldInput, Object newInput) {
            }
        }
    }
}
