/*******************************************************************************
 * Copyright (c) 2010 Neil Bartlett.
 * All rights reserved. This program and the accompanying materials
 * are made available under the terms of the Eclipse Public License v1.0
 * which accompanies this distribution, and is available at
 * http://www.eclipse.org/legal/epl-v10.html
 *
 * Contributors:
 *     Neil Bartlett - initial API and implementation
 *******************************************************************************/
package bndtools.editor.pkgpatterns;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;

import org.bndtools.utils.collections.CollectionUtils;
import org.eclipse.jface.util.LocalSelectionTransfer;
import org.eclipse.jface.viewers.ArrayContentProvider;
import org.eclipse.jface.viewers.IBaseLabelProvider;
import org.eclipse.jface.viewers.ISelection;
import org.eclipse.jface.viewers.ISelectionChangedListener;
import org.eclipse.jface.viewers.ISelectionProvider;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.jface.viewers.SelectionChangedEvent;
import org.eclipse.jface.viewers.StructuredSelection;
import org.eclipse.jface.viewers.TableViewer;
import org.eclipse.swt.SWT;
import org.eclipse.swt.dnd.DND;
import org.eclipse.swt.dnd.TextTransfer;
import org.eclipse.swt.dnd.Transfer;
import org.eclipse.swt.events.FocusAdapter;
import org.eclipse.swt.events.FocusEvent;
import org.eclipse.swt.events.KeyAdapter;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.SelectionAdapter;
import org.eclipse.swt.events.SelectionEvent;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Table;
import org.eclipse.swt.widgets.ToolBar;
import org.eclipse.swt.widgets.ToolItem;
import org.eclipse.ui.ISharedImages;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.forms.IManagedForm;
import org.eclipse.ui.forms.SectionPart;
import org.eclipse.ui.forms.editor.IFormPage;
import org.eclipse.ui.forms.widgets.FormToolkit;
import org.eclipse.ui.forms.widgets.Section;
import org.eclipse.ui.part.ResourceTransfer;
import org.eclipse.ui.plugin.AbstractUIPlugin;
import org.osgi.framework.Constants;

import aQute.bnd.build.model.BndEditModel;
import aQute.bnd.build.model.clauses.HeaderClause;
import bndtools.Plugin;
import bndtools.editor.common.PackageDropAdapter;

public abstract class PkgPatternsListPart<C extends HeaderClause> extends SectionPart implements PropertyChangeListener {

    protected static final String PROP_SELECTION = "selection";
    private final PropertyChangeSupport propChangeSupport = new PropertyChangeSupport(this);

    private final String propertyName;
    private final IBaseLabelProvider labelProvider;
    protected ArrayList<C> clauses = new ArrayList<C>();

    private IManagedForm managedForm;
    private TableViewer viewer;
    private BndEditModel model;

    private List<C> selection;

    private final Image imgAnalyse = AbstractUIPlugin.imageDescriptorFromPlugin(Plugin.PLUGIN_ID, "/icons/cog_go.png")
        .createImage();
    private final Image imgInsert = AbstractUIPlugin.imageDescriptorFromPlugin(Plugin.PLUGIN_ID, "/icons/table_row_insert.png")
        .createImage();
    private final Image imgUp = AbstractUIPlugin.imageDescriptorFromPlugin(Plugin.PLUGIN_ID, "/icons/arrow_up.png")
        .createImage();
    private final Image imgDown = AbstractUIPlugin.imageDescriptorFromPlugin(Plugin.PLUGIN_ID, "/icons/arrow_down.png")
        .createImage();

    protected final String title;

    public PkgPatternsListPart(Composite parent, FormToolkit toolkit, int style, String propertyName, String title, IBaseLabelProvider labelProvider) {
        super(parent, toolkit, style);
        this.propertyName = propertyName;
        this.title = title;
        this.labelProvider = labelProvider;

        Section section = getSection();
        section.setText(title);
        createSection(section, toolkit);
    }

    protected void createSection(Section section, FormToolkit toolkit) {
        Composite composite = toolkit.createComposite(section);
        section.setClient(composite);

        ToolBar toolbar = new ToolBar(section, SWT.FLAT);
        section.setTextClient(toolbar);
        final ToolItem addItem = new ToolItem(toolbar, SWT.PUSH);
        addItem.setImage(PlatformUI.getWorkbench()
            .getSharedImages()
            .getImage(ISharedImages.IMG_OBJ_ADD));
        addItem.setToolTipText("Add");

        final ToolItem insertItem = new ToolItem(toolbar, SWT.PUSH);
        insertItem.setImage(imgInsert);
        insertItem.setToolTipText("Insert");
        insertItem.setEnabled(false);

        final ToolItem removeItem = new ToolItem(toolbar, SWT.PUSH);
        removeItem.setImage(PlatformUI.getWorkbench()
            .getSharedImages()
            .getImage(ISharedImages.IMG_TOOL_DELETE));
        removeItem.setDisabledImage(PlatformUI.getWorkbench()
            .getSharedImages()
            .getImage(ISharedImages.IMG_TOOL_DELETE_DISABLED));
        removeItem.setToolTipText("Remove");
        removeItem.setEnabled(false);

        Table table = toolkit.createTable(composite, SWT.MULTI | SWT.FULL_SELECTION | SWT.BORDER);
        viewer = new TableViewer(table);
        viewer.setUseHashlookup(false);
        viewer.setContentProvider(new ArrayContentProvider());
        viewer.setLabelProvider(labelProvider);

        toolbar = new ToolBar(composite, SWT.FLAT | SWT.HORIZONTAL | SWT.RIGHT);

        final ToolItem btnMoveUp = new ToolItem(toolbar, SWT.PUSH);
        btnMoveUp.setText("Up");
        btnMoveUp.setImage(imgUp);
        btnMoveUp.setEnabled(false);

        final ToolItem btnMoveDown = new ToolItem(toolbar, SWT.PUSH);
        btnMoveDown.setText("Down");
        btnMoveDown.setImage(imgDown);
        btnMoveDown.setEnabled(false);

        // Listeners
        table.addFocusListener(new FocusAdapter() {
            @Override
            public void focusGained(FocusEvent e) {
                ISelection selection = viewer.getSelection();
                if (!selection.isEmpty())
                    managedForm.fireSelectionChanged(PkgPatternsListPart.this, selection);
            }
        });
        viewer.addSelectionChangedListener(new ISelectionChangedListener() {
            @Override
            public void selectionChanged(SelectionChangedEvent event) {
                List<C> oldSelection = selection;
                IStructuredSelection structSel = (IStructuredSelection) event.getSelection();

                @SuppressWarnings("unchecked")
                List<C> newSelection = structSel.toList();
                selection = newSelection;

                propChangeSupport.firePropertyChange(PROP_SELECTION, oldSelection, selection);

                managedForm.fireSelectionChanged(PkgPatternsListPart.this, event.getSelection());
                boolean enabled = !viewer.getSelection()
                    .isEmpty();
                insertItem.setEnabled(enabled);
                removeItem.setEnabled(enabled);
                btnMoveUp.setEnabled(enabled);
                btnMoveDown.setEnabled(enabled);
            }
        });
        viewer.addDropSupport(DND.DROP_COPY | DND.DROP_MOVE, new Transfer[] {
            LocalSelectionTransfer.getTransfer(), ResourceTransfer.getInstance(), TextTransfer.getInstance()
        }, new PackageDropAdapter<C>(viewer) {
            @Override
            protected C createNewEntry(String packageName) {
                return newHeaderClause(packageName);
            }

            @Override
            protected void addRows(int index, Collection<C> rows) {
                doAddClauses(rows, index, true);
            }

            @Override
            protected int indexOf(Object object) {
                return clauses.indexOf(object);
            }
        });
        table.addKeyListener(new KeyAdapter() {
            @Override
            public void keyReleased(KeyEvent e) {
                if (e.character == SWT.DEL) {
                    doRemoveSelectedClauses();
                } else if (e.character == '+') {
                    doAddClausesAfterSelection(generateClauses());
                }
            }
        });
        addItem.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                doAddClausesAfterSelection(generateClauses());
            }
        });
        insertItem.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                doInsertClausesAtSelection(generateClauses());
            }
        });
        removeItem.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                doRemoveSelectedClauses();
            }
        });
        btnMoveUp.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                doMoveUp();
            }
        });
        btnMoveDown.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent e) {
                doMoveDown();
            }
        });

        // Layout
        GridLayout layout;

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

        GridData gd = new GridData(SWT.FILL, SWT.FILL, true, true);
        gd.widthHint = 75;
        gd.heightHint = 75;
        table.setLayoutData(gd);
        toolbar.setLayoutData(new GridData(SWT.RIGHT, SWT.CENTER, false, false));
    }

    protected abstract C newHeaderClause(String text);

    protected abstract List<C> loadFromModel(BndEditModel model);

    protected abstract void saveToModel(BndEditModel model, List<? extends C> clauses);

    protected List<C> getClauses() {
        return clauses;
    }

    protected Collection<? extends C> generateClauses() {
        Collection<C> result = new ArrayList<C>();
        result.add(newHeaderClause(""));
        return result;
    }

    /**
     * Add the specified clauses to the view.
     *
     * @param newClauses The new clauses.
     * @param index The index at which to insert the new clauses OR -1 to append at the end.
     */
    protected void doAddClauses(Collection<? extends C> newClauses, int index, boolean select) {
        Object[] newClausesArray = newClauses.toArray();

        if (index == -1 || index == this.clauses.size()) {
            clauses.addAll(newClauses);
            viewer.add(newClausesArray);
        } else {
            clauses.addAll(index, newClauses);
            viewer.refresh();
        }

        if (select)
            viewer.setSelection(new StructuredSelection(newClausesArray), true);
        validate();
        markDirty();
    }

    private void doAddClausesAfterSelection(Collection<? extends C> newClauses) {
        if (newClauses != null && !newClauses.isEmpty()) {
            int selectedIndex = -1;
            IStructuredSelection selection = (IStructuredSelection) viewer.getSelection();
            if (!selection.isEmpty()) {
                // find the highest selected index
                for (Iterator<?> iter = selection.iterator(); iter.hasNext();) {
                    int index = this.clauses.indexOf(iter.next());
                    if (index > selectedIndex)
                        selectedIndex = index;
                }
            }
            doAddClauses(newClauses, selectedIndex, true);
        }
    }

    private void doInsertClausesAtSelection(Collection<? extends C> newClauses) {
        if (newClauses != null && !newClauses.isEmpty()) {
            int selectedIndex;
            IStructuredSelection selection = (IStructuredSelection) viewer.getSelection();
            if (selection.isEmpty())
                return;
            selectedIndex = this.clauses.indexOf(selection.getFirstElement());

            doAddClauses(newClauses, selectedIndex, true);
        }
    }

    protected void doRemoveClauses(List<?> toRemove) {
        clauses.removeAll(toRemove);
        viewer.remove(toRemove.toArray());

        validate();
        markDirty();
    }

    private void doRemoveSelectedClauses() {
        doRemoveClauses(((IStructuredSelection) viewer.getSelection()).toList());
        validate();
        markDirty();
    }

    void doMoveUp() {
        int[] selectedIndexes = findSelectedIndexes();
        if (CollectionUtils.moveUp(clauses, selectedIndexes)) {
            viewer.refresh();
            validate();
            markDirty();
        }
    }

    void doMoveDown() {
        int[] selectedIndexes = findSelectedIndexes();
        if (CollectionUtils.moveDown(clauses, selectedIndexes)) {
            viewer.refresh();
            validate();
            markDirty();
        }
    }

    int[] findSelectedIndexes() {
        Object[] selection = ((IStructuredSelection) viewer.getSelection()).toArray();
        int[] selectionIndexes = new int[selection.length];

        for (int i = 0; i < selection.length; i++) {
            selectionIndexes[i] = clauses.indexOf(selection[i]);
        }
        return selectionIndexes;
    }

    @Override
    public void initialize(IManagedForm form) {
        super.initialize(form);

        this.managedForm = form;
        this.model = (BndEditModel) form.getInput();
        this.model.addPropertyChangeListener(Constants.IMPORT_PACKAGE, this);
    }

    @Override
    public void dispose() {
        super.dispose();
        this.model.removePropertyChangeListener(Constants.IMPORT_PACKAGE, this);
        imgAnalyse.dispose();
        imgInsert.dispose();
        imgUp.dispose();
        imgDown.dispose();
    }

    @Override
    public void refresh() {
        super.refresh();

        // Deep-copy the model
        Collection<C> tmp = loadFromModel(model);
        if (tmp != null) {
            clauses = new ArrayList<C>(tmp.size());
            for (C clause : tmp) {
                @SuppressWarnings("unchecked")
                C clone = (C) clause.clone();
                clauses.add(clone);
            }
        } else {
            clauses = new ArrayList<C>();
        }
        viewer.setInput(clauses);
        validate();
    }

    public void validate() {
        // Do nothing.
    }

    @Override
    public void commit(boolean onSave) {
        try {
            model.removePropertyChangeListener(propertyName, this);
            saveToModel(model, clauses.isEmpty() ? null : clauses);
        } finally {
            super.commit(onSave);
            model.addPropertyChangeListener(propertyName, this);
        }
    }

    @Override
    public void propertyChange(PropertyChangeEvent evt) {
        IFormPage page = (IFormPage) managedForm.getContainer();
        if (page.isActive())
            refresh();
        else
            markStale();
    }

    public void updateLabels(Collection<?> elements) {
        updateLabels(elements.toArray());
    }

    public void updateLabels(Object[] elements) {
        viewer.update(elements, null);
    }

    public ISelectionProvider getSelectionProvider() {
        return viewer;
    }

    public List<C> getSelection() {
        return selection;
    }

    public void setSelection(List<C> selection) {
        List<C> oldSelection = selection;
        this.selection = selection;
        propChangeSupport.firePropertyChange(PROP_SELECTION, oldSelection, selection);
    }

    public void addPropertyChangeListener(PropertyChangeListener l) {
        propChangeSupport.addPropertyChangeListener(l);
    }

    public void removePropertyChangeListener(PropertyChangeListener l) {
        propChangeSupport.addPropertyChangeListener(l);
    }

    public void addPropertyChangeListener(String name, PropertyChangeListener l) {
        propChangeSupport.addPropertyChangeListener(name, l);
    }

    public void removePropertyChangeListener(String name, PropertyChangeListener l) {
        propChangeSupport.addPropertyChangeListener(name, l);
    }

}
