/*--------------------------------------------------------------*
  Copyright (C) 2006-2015 OpenSim Ltd.

  This file is distributed WITHOUT ANY WARRANTY. See the file
  'License' for details on this and other legal matters.
*--------------------------------------------------------------*/

package org.omnetpp.common.ui;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.ListenerList;
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.swt.SWT;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.GC;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;

/**
 * Holds several controls that can be selected and
 * reordered with the mouse. Typically the user wants to
 * set a GridLayout on the control, but this is not mandatory.
 *
 * <pre>
 * GridLayout gridLayout = new GridLayout(4, true);
 * gridLayout.horizontalSpacing = gridLayout.verticalSpacing = 5;
 * liveTable.setLayout(gridLayout);
 * </pre>
 *
 * @author andras
 */
//XXX move to common plug-in
public class LiveTable extends Composite  implements ISelectionProvider {
    private static final Color SELECTBORDER_COLOR = new Color(null,255,0,0);
    private static final Color INSERTMARK_COLOR = new Color(null,0,0,0);

    private ArrayList<Control> orderedChildren = new ArrayList<Control>();
    private ArrayList<Control> selection = new ArrayList<Control>();
    private ListenerList<ISelectionChangedListener> selectionChangedListeners = new ListenerList<>();
    private ListenerList<IChildOrderChangedListener> childOrderChangedListeners = new ListenerList<>();
    private Control insertMark = null;

    /**
     * Constructor.
     */
    public LiveTable(Composite parent, int style) {
        super(parent, style);

        addPaintListener(new PaintListener() {
            public void paintControl(PaintEvent e) {
                redrawCanvas(e.gc);
            }
        });
    }

    /**
     * Overridden so that we can reorder the children.
     */
    @Override
    public Control[] getChildren() {
        // synchronize orderedChildren to actual children; remove disposed ones, add new ones
        Control[] children = super.getChildren();
        for (Iterator<Control> childIterator = orderedChildren.iterator(); childIterator.hasNext(); )
            if (childIterator.next().isDisposed())
                childIterator.remove();
        for (Control child : children)
            if (!orderedChildren.contains(child))
                orderedChildren.add(child);
        return orderedChildren.toArray(new Control[orderedChildren.size()]);
    }

    public void setChildOrder(List<? extends Control> childOrder) {
        // TODO check children
        orderedChildren.clear();
        orderedChildren.addAll(childOrder);
    }

    /**
     * Adds the necessary mouse listeners for dragging and selecting children.
     */
    public void configureChild(Control control) {
        control.addMouseMoveListener(new MouseMoveListener() {
            public void mouseMove(MouseEvent e) {
                if ((e.stateMask & SWT.BUTTON_MASK) != 0) { // dragging
                    Control sender = (Control)e.widget;
                    Control c = findControlUnder(sender.toDisplay(e.x,e.y));
                    if (c!=null)
                        setInsertMark(c);
                }
            }
        });
        control.addMouseListener(new MouseListener() {
            public void mouseDoubleClick(MouseEvent e) {}
            public void mouseDown(MouseEvent e) {
                if (selection.size()<=1 && (e.stateMask & SWT.MOD1)==0)
                    setSelection((Control)e.widget);
            }
            public void mouseUp(MouseEvent e) {
                Control sender = (Control)e.widget;
                Control target = findControlUnder(sender.toDisplay(e.x,e.y));
                if (sender!=target && target!=null) {
                    // this is a move
                    if (selection.isEmpty())
                        moveChild(sender, target);
                    else
                        moveSelectionTo(target);
                }
                if (sender==target) {
                    // this is a selection
                    if ((e.stateMask & SWT.MOD1)!=0) {
                        if (!selection.contains(e.widget))
                            select((Control)e.widget);
                        else
                            deselect((Control)e.widget);
                    }
                    else {
                        setSelection((Control)e.widget);
                    }
                }
                setInsertMark(null);
            }
        });
    }

    /**
     * Returns the child control under the given mouse position, or null.
     * Expects screen coordinates.
     */
    private Control findControlUnder(Point p) {
        for (Control child : getChildren()) {
            Point topLeft = child.toDisplay(0,0);
            Rectangle bounds = child.getBounds();
            bounds.x = topLeft.x;
            bounds.y = topLeft.y;
            if (bounds.contains(p)) {
                return child;
            }
        }
        return null;
    }

    /**
     * Displays the insert mark around the given item.
     * A null value means no insert mark.
     */
    public void setInsertMark(Control item) {
        Assert.isTrue(item==null || item.getParent()==this);
        insertMark = item;
        redraw();
    }

    /**
     * Redraws the canvas, with selection marks and insert mark.
     */
    private void redrawCanvas(GC gc) {
        for (Control child : selection) {
            Rectangle bounds = child.getBounds();
            bounds.x -= 2;
            bounds.y -= 2;
            bounds.width += 3;
            bounds.height += 3;
            gc.setForeground(SELECTBORDER_COLOR);
            gc.drawRectangle(bounds);
        }
        if (insertMark!=null) {
            Rectangle bounds = insertMark.getBounds();
            bounds.x -= 2;
            bounds.y -= 2;
            bounds.width += 3;
            bounds.height += 3;
            gc.setForeground(INSERTMARK_COLOR);
            gc.drawRectangle(bounds);
        }
    }

    /**
     * Changes the order of children so that "item" is moved to
     * where the "target" child currently is.
     */
    public void moveChild(Control item, Control atItem) {
        Assert.isTrue(item.getParent()==this);
        Assert.isTrue(atItem.getParent()==this);
        if (item==atItem)
            return;

        moveTo(Collections.singletonList(item), atItem);
        layout();
    }

    /**
     * Changes the order of children so that the selected items
     * are moved to where the "target" child currently is.
     */
    protected void moveSelectionTo(Control target) {
        Assert.isTrue(target.getParent()==this);
        moveTo(selection, target);
        layout();
    }

    protected void moveTo(List<Control> controls, Control target) {
        int index = orderedChildren.indexOf(target);
        for (Control control : controls)
            orderedChildren.remove(control);
        if (index>orderedChildren.size())
            index = orderedChildren.size();
        for (Control control : controls)
            orderedChildren.add(index++, control);

        fireChildOrderChanged();
    }

    private void addToSelection(Control control) {
        Assert.isTrue(control == null || control.getParent()==this);
        if (control!=null && !selection.contains(control))
            selection.add(control);
    }

    /**
     * Extends the selection with the given control.
     */
    public void select(Control control) {
        addToSelection(control);
        redraw();
        fireSelectionChanged();
    }

    /**
     * Removes the given control from the selection.
     */
    public void deselect(Control control) {
        if (selection.contains(control))
            selection.remove(control);
        redraw();
        fireSelectionChanged();
    }

    /**
     * Extends the selection with the given controls.
     */
    public void select(Control[] controls) {
        for (Control control : controls)
            addToSelection(control);
        redraw();
        fireSelectionChanged();
    }

    /**
     * Sets the selection to the given control.
     */
    public void setSelection(Control control) {
        selection.clear();
        select(control);
    }

    /**
     * Sets the selection to the given controls.
     */
    public void setSelection(Control[] controls) {
        selection.clear();
        select(controls);
    }

    /**
     * Selects all controls.
     */
    public void selectAll() {
        selection.clear();
        for (Control child: getChildren())
            selection.add(child);
    }

    /**
     * Returns the selection.
     */
    public ISelection getSelection() {
        return new StructuredSelection(selection);
    }

    /**
     * Sets the current selection for this selection provider.
     */
    public void setSelection(ISelection selection) {
        List<Control> controls = new ArrayList<Control>();
        if (selection instanceof IStructuredSelection) {
            for (Iterator<?> iterator = ((IStructuredSelection)selection).iterator(); iterator.hasNext();) {
                Object element = iterator.next();
                if (element instanceof Control)
                    controls.add((Control)element);
            }
        }
        setSelection(controls.toArray(new Control[controls.size()]));
    }

    /**
     * Adds a listener for selection changes in this selection provider.
     */
    public void addSelectionChangedListener(ISelectionChangedListener listener) {
        selectionChangedListeners.add(listener);
    }

    /**
     * Removes the given selection change listener from this selection provider.
     */
    public void removeSelectionChangedListener(ISelectionChangedListener listener) {
        selectionChangedListeners.remove(listener);
    }

    /**
     * Notifies the selection change listeners about a selection change event.
     */
    protected void fireSelectionChanged() {
        SelectionChangedEvent event = new SelectionChangedEvent(this, getSelection());
        for (ISelectionChangedListener listener : selectionChangedListeners)
            listener.selectionChanged(event);
    }

    public void addChildOrderChangedListener(IChildOrderChangedListener listener) {
        childOrderChangedListeners.add(listener);
    }

    public void removeChildOrderChangedListener(IChildOrderChangedListener listener) {
        childOrderChangedListeners.remove(listener);
    }

    protected void fireChildOrderChanged() {
        for (IChildOrderChangedListener listener : childOrderChangedListeners)
            listener.childOrderChanged(this);
    }

    public static interface IChildOrderChangedListener {
        void childOrderChanged(LiveTable table);
    }
}
