/******************************************************************************
 * Copyright (c) 2006, Intalio Inc.
 * 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:
 *     Intalio Inc. - initial API and implementation
 *******************************************************************************/

/** 
 * Date                 Author              Changes 
 * 17 Nov 2006      MPeleshchyshyn      Created 
 **/
package org.eclipse.stp.bpmn.diagram.actions;

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;

import org.eclipse.core.commands.ExecutionException;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.IAdaptable;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.IStatus;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.draw2d.geometry.Point;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.emf.common.util.EList;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.ecore.resource.Resource;
import org.eclipse.emf.transaction.TransactionalEditingDomain;
import org.eclipse.emf.workspace.util.WorkspaceSynchronizer;
import org.eclipse.gef.EditPart;
import org.eclipse.gmf.runtime.common.core.command.CompositeCommand;
import org.eclipse.gmf.runtime.common.core.command.ICommand;
import org.eclipse.gmf.runtime.diagram.core.edithelpers.CreateElementRequestAdapter;
import org.eclipse.gmf.runtime.diagram.core.util.ViewUtil;
import org.eclipse.gmf.runtime.diagram.ui.commands.CommandProxy;
import org.eclipse.gmf.runtime.diagram.ui.commands.CreateCommand;
import org.eclipse.gmf.runtime.diagram.ui.commands.ICommandProxy;
import org.eclipse.gmf.runtime.diagram.ui.commands.SetBoundsCommand;
import org.eclipse.gmf.runtime.diagram.ui.editparts.IGraphicalEditPart;
import org.eclipse.gmf.runtime.diagram.ui.editparts.ShapeNodeEditPart;
import org.eclipse.gmf.runtime.diagram.ui.internal.commands.ToggleCanonicalModeCommand;
import org.eclipse.gmf.runtime.diagram.ui.parts.IDiagramEditDomain;
import org.eclipse.gmf.runtime.diagram.ui.requests.CreateViewRequest;
import org.eclipse.gmf.runtime.emf.core.util.EObjectAdapter;
import org.eclipse.gmf.runtime.emf.type.core.commands.CreateElementCommand;
import org.eclipse.gmf.runtime.emf.type.core.commands.MoveElementsCommand;
import org.eclipse.gmf.runtime.emf.type.core.requests.CreateElementRequest;
import org.eclipse.gmf.runtime.emf.type.core.requests.MoveRequest;
import org.eclipse.gmf.runtime.notation.Node;
import org.eclipse.gmf.runtime.notation.View;
import org.eclipse.jface.viewers.IStructuredSelection;
import org.eclipse.stp.bpmn.diagram.edit.parts.LaneEditPart;
import org.eclipse.stp.bpmn.diagram.edit.parts.PoolPoolCompartmentEditPart;
import org.eclipse.stp.bpmn.diagram.edit.parts.SequenceEdgeEditPart;
import org.eclipse.stp.bpmn.diagram.edit.parts.SubProcessEditPart;
import org.eclipse.stp.bpmn.diagram.edit.parts.SubProcessSubProcessBodyCompartmentEditPart;
import org.eclipse.stp.bpmn.diagram.part.BpmnDiagramEditorPlugin;
import org.eclipse.stp.bpmn.diagram.providers.BpmnElementTypes;
import org.eclipse.stp.bpmn.figures.SubProcessBorderFigure;
import org.eclipse.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPart;

/**
 * This class groups actions
 * 
 * @author MPeleshchyshyn
 * @author <a href="http://www.intalio.com">&copy; Intalio, Inc.</a>
 */
public class GroupAction extends AbstractGroupUngroupAction {
    public static final String ACTION_ID = "groupAction";

    public static final String TOOLBAR_ACTION_ID = "toolbarGroupAction";

    static final String ICON_PATH = "icons/Group.gif";

    private static final String ACTION_TEXT = "Group";

    static final String TOOLTIP_TEXT = "Group Shapes(Create Sub-Process)";

    public GroupAction(IWorkbenchPage workbenchPage) {
        super(workbenchPage);
    }

    public GroupAction(IWorkbenchPart workbenchPart) {
        super(workbenchPart);
    }

    private static GroupAction createActionWithoutId(
            IWorkbenchPage workbenchPage) {
        GroupAction action = new GroupAction(workbenchPage);
        action.setText(ACTION_TEXT);
        action.setToolTipText(TOOLTIP_TEXT);
        action.setImageDescriptor(BpmnDiagramEditorPlugin.getInstance()
                .findImageDescriptor(ICON_PATH));

        return action;
    }

    public static GroupAction createGroupAction(IWorkbenchPage workbenchPage) {
        GroupAction action = createActionWithoutId(workbenchPage);
        action.setId(ACTION_ID);

        return action;
    }

    public static GroupAction createToolbarGroupAction(
            IWorkbenchPage workbenchPage) {
        GroupAction action = createActionWithoutId(workbenchPage);
        action.setId(TOOLBAR_ACTION_ID);

        return action;
    }

    /**
     * Edit part to be grouped
     */
    private List<IGraphicalEditPart> editParts = new ArrayList<IGraphicalEditPart>();

    /**
     * External source connections
     */
    private Collection<SequenceEdgeEditPart> externalSrcConnection = new ArrayList<SequenceEdgeEditPart>();

    /**
     * External target connections
     */
    private Collection<SequenceEdgeEditPart> externalTgtConnections = new ArrayList<SequenceEdgeEditPart>();

    /**
     * Internal connections - between edit parts to be grouped
     */
    private Collection<SequenceEdgeEditPart> internalConnections = new HashSet<SequenceEdgeEditPart>();

    /*
     * (non-Javadoc)
     * 
     * @see org.eclipse.ui.IActionDelegate#run(org.eclipse.jface.action.IAction)
     */
    @Override
    protected void doRun(IProgressMonitor progressMonitor) {
        IGraphicalEditPart firstEditPart = editParts.get(0);

        // common parent of selected edit part now will become a parent for new
        // subprocess
        EditPart containerEditPart = firstEditPart.getParent();
        View container = (View) containerEditPart.getModel();
        EObject context = ViewUtil.resolveSemanticElement(container);

        TransactionalEditingDomain domain = firstEditPart.getEditingDomain();
        IDiagramEditDomain diagramEditDomain = firstEditPart
                .getDiagramEditDomain();
        // fisrt let's create new sub-process
        CreateElementRequest createRequest = new CreateElementRequest(domain,
                context, BpmnElementTypes.SubProcess_2002);
        CreateElementCommand cmd = new CreateElementCommand(createRequest);
        CreateViewRequest.ViewDescriptor viewDescriptor = new CreateViewRequest.ViewDescriptor(
                new CreateElementRequestAdapter(createRequest), Node.class,
                Integer.toString(SubProcessEditPart.VISUAL_ID), -1, true,
                firstEditPart.getDiagramPreferencesHint());
        CreateCommand createCommand = new CreateCommand(domain, viewDescriptor,
                container);
        CompositeCommand cc = new CompositeCommand("Create new subprocess");
        // first disbale canonical mode
        ToggleCanonicalModeCommand canonicalCmd = new ToggleCanonicalModeCommand(
                context, false);
        cc.compose(new CommandProxy(canonicalCmd));
        cc.compose(cmd);
        cc.compose(createCommand);

        // enable canonical mode again
        cc.compose(new CommandProxy(ToggleCanonicalModeCommand
                .getToggleCanonicalModeCommand(canonicalCmd, true)));
        try {
            cc.execute(new NullProgressMonitor(), null);
            // now let's reparent all selected nodes and connections
            View subProcessView = (View) viewDescriptor.getAdapter(View.class);
            SubProcessEditPart subProcessEditPart = null;
            List chilren = containerEditPart.getChildren();
            for (Object child : chilren) {
                if (((EditPart) child).getModel() == subProcessView) {
                    subProcessEditPart = (SubProcessEditPart) child;
                    break;
                }
            }
            cc = new GroupCommand("Group", subProcessEditPart);
            Collection tmp = new ArrayList();
            tmp.add(subProcessEditPart);
            tmp.add(containerEditPart);
            // first disable canonical mode
            canonicalCmd = ToggleCanonicalModeCommand
                    .getToggleCanonicalModeCommand(tmp, false);
            cc.compose(new CommandProxy(canonicalCmd));

            // let's find created subprocess' body compartment model
            View subProcessBodyView = getSubProcessBodyView(subProcessView);

            // now let's reparent selected elements and claculate selection
            // bounds
            Rectangle selectionBounds = null;
            for (IGraphicalEditPart editPart : editParts) {
                View elementView = (View) editPart.getModel();
                EObject element = ViewUtil.resolveSemanticElement(elementView);
                ICommand reparentViewCmd = new AddCommand(editPart
                        .getEditingDomain(), new EObjectAdapter(
                        subProcessBodyView), new EObjectAdapter(elementView));
                cc.compose(reparentViewCmd);
                ICommand reparentCommand = getSemanticReparentCommand(editPart
                        .getEditingDomain(), element, createRequest
                        .getNewElement());
                cc.compose(reparentCommand);
                if (selectionBounds == null) {
                    selectionBounds = editPart.getFigure().getBounds()
                            .getCopy();
                } else {
                    selectionBounds.union(editPart.getFigure().getBounds());
                }
            }

            // change location of reparented edit parts and change size of
            // created subprocess itself
            addChangeBoundsCommands(selectionBounds.getLocation(), cc);
            cc.compose(getSubprocessSetBoundsCommand(subProcessEditPart,
                    selectionBounds));

            // location of the
            // created sub-process
            // now reparent all inner connections
            reparentInnnerConnections(internalConnections, domain,
                    createRequest.getNewElement(), cc);

            // we need this before external connection reparenting (used for
            // anchors calculations)
            subProcessEditPart.getFigure().setBounds(selectionBounds);
            // now reparent external connections
            ICommand reparentExtConnCmd = getReparentExternalConnectionsCommand(subProcessEditPart);
            if (reparentExtConnCmd != null) {
                cc.compose(reparentExtConnCmd);
            }
            // now turn on canonical mode
            cc.compose(new CommandProxy(canonicalCmd
                    .getToggleCanonicalModeCommand(canonicalCmd, true)));
            diagramEditDomain.getDiagramCommandStack().execute(
                    new ICommandProxy(cc));
            // cc.execute(new NullProgressMonitor(), null);

        } catch (ExecutionException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }

    /**
     * Create composite command for external connections reparenting.
     * 
     * @param subProcessEditPart
     *            new container
     * @return composite command for external source and targe connections
     *         reparenting
     */
    private ICommand getReparentExternalConnectionsCommand(
            ShapeNodeEditPart subProcessEditPart) {
        CompositeCommand cc = new CompositeCommand(
                "Reparent extenal connections");
        int size = externalSrcConnection.size();
        int idx = 0;
        for (SequenceEdgeEditPart connection : externalSrcConnection) {
            cc.compose(getReconnectSourceCommand(connection,
                    subProcessEditPart, idx, size));
            idx++;
        }
        idx = 0;
        size = externalTgtConnections.size();
        for (SequenceEdgeEditPart connection : externalTgtConnections) {
            cc.compose(getReconnectTargetCommand(connection,
                    subProcessEditPart, idx, size));
            idx++;
        }
        return !cc.isEmpty() ? cc : null;
    }

    /**
     * Calculates bounds for the specified subprocess to hold elements with the
     * specified selection bounds.
     * 
     * @param subProcessEditPart
     *            the sub-process
     * @param selectionBounds
     *            selection bounds
     * @return <code>setBoundsCommand</code> for the specified sub-process.
     */
    private static ICommand getSubprocessSetBoundsCommand(
            ShapeNodeEditPart subProcessEditPart, Rectangle selectionBounds) {
        // selectionBounds = selectionBounds.getCopy();

        selectionBounds.shrink(-5, -5);
        if (selectionBounds.x < 0) {
            selectionBounds.x = 0;
        }
        if (selectionBounds.y < 0) {
            selectionBounds.y = 0;
        }
        selectionBounds.height = selectionBounds.height
                + SubProcessBorderFigure.getFixedHeightDP(subProcessEditPart
                        .getFigure());
        return new SetBoundsCommand(subProcessEditPart.getEditingDomain(),
                "Set sub-process bounds", subProcessEditPart, selectionBounds);

    }

    /**
     * Searched subprocess body view (model) for the specified sub process view.
     * 
     * @param subProcessView
     *            the subprocess view
     * @return subprocess body view
     */
    private static View getSubProcessBodyView(View subProcessView) {
        EList children = subProcessView.getPersistedChildren();
        for (Object object : children) {
            View child = (View) object;
            if (child
                    .getType()
                    .equals(
                            Integer
                                    .toString(SubProcessSubProcessBodyCompartmentEditPart.VISUAL_ID))) {
                return child;
            }
        }
        return null;
    }

    /**
     * Shifts selected edit parts (subtracts specified selection bounds
     * location).
     * 
     * @param selectionBoundsLocation
     *            top-left coordinate of the selceted objects before reparenting
     * @param cc
     *            the composite command to append commands
     */
    private void addChangeBoundsCommands(Point selectionBoundsLocation,
            CompositeCommand cc) {
        CompositeCommand shiftCommands = new CompositeCommand(
                "Shift selected nodes");
        cc.compose(shiftCommands);
        for (IGraphicalEditPart editPart : editParts) {
            Point p = editPart.getFigure().getBounds().getLocation();
            p.translate(-selectionBoundsLocation.x, -selectionBoundsLocation.y);
            SetBoundsCommand cmd = new SetBoundsCommand(editPart
                    .getEditingDomain(), "Set new editpart location", editPart,
                    p);
            shiftCommands.compose(cmd);
        }
    }

    /**
     * Checks all source and target connections of the specified edit part and
     * sets it into one of the specified collections. If connection connects the
     * specified object with some other selected element it is added to internal
     * connection. In other case it is added to external source or external
     * target connections.
     * 
     * @param editPart
     *            the edit part
     * @param internalConnections
     *            set that holds internal connections
     * @param externalSrcConnection
     *            holds external source connections
     * @param externalTgtConnections
     *            holds external target connections
     */
    private void sortConnections(IGraphicalEditPart editPart,
            Collection<SequenceEdgeEditPart> internalConnections,
            Collection<SequenceEdgeEditPart> externalSrcConnection,
            Collection<SequenceEdgeEditPart> externalTgtConnections) {
        List srcConnections = editPart.getSourceConnections();
        for (Object connection : srcConnections) {
            if (connection instanceof SequenceEdgeEditPart) {
                SequenceEdgeEditPart sequenceConnection = (SequenceEdgeEditPart) connection;
                if (editParts.contains(sequenceConnection.getTarget())) {
                    internalConnections.add(sequenceConnection);
                } else {
                    externalSrcConnection.add(sequenceConnection);
                }
            }
        }
        List targetConnections = editPart.getTargetConnections();
        for (Object connection : targetConnections) {
            if (connection instanceof SequenceEdgeEditPart) {
                SequenceEdgeEditPart sequenceConnection = (SequenceEdgeEditPart) connection;
                if (editParts.contains(sequenceConnection.getSource())) {
                    internalConnections.add(sequenceConnection);
                } else {
                    externalTgtConnections.add(sequenceConnection);
                }
            }
        }
    }

    /**
     * Creates semantic reparent command.
     * 
     * @param domain
     *            transactional domain
     * @param element
     *            element to be reparented
     * @param container
     *            the new container
     * @return mew <code>MoveElementsCommand</code> that reparents elemnent
     *         semantically
     */
    private ICommand getSemanticReparentCommand(
            TransactionalEditingDomain domain, EObject element,
            EObject container) {
        MoveRequest moveRequest = new MoveRequest(domain, container, element);
        MoveElementsCommand semanticMoveCmd = new MoveElementsCommand(
                moveRequest);
        return semanticMoveCmd;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.eclipse.ui.IActionDelegate#selectionChanged(org.eclipse.jface.action.IAction,
     *      org.eclipse.jface.viewers.ISelection)
     */
    public void refresh() {
        editParts.clear();
        internalConnections.clear();
        externalSrcConnection.clear();
        externalTgtConnections.clear();

        IStructuredSelection strSelection = getStructuredSelection();
        Iterator iter = strSelection.iterator();
        while (iter.hasNext()) {
            Object editPart = iter.next();
            if (editPart instanceof IGraphicalEditPart) {
                editParts.add((IGraphicalEditPart) editPart);
            }
        }

        EditPart parentContainer = null;
        Iterator<IGraphicalEditPart> iterator = editParts.iterator();
        while (iterator.hasNext()) {
            IGraphicalEditPart editPart = iterator.next();
            EditPart container = editPart.getParent();
            if (editPart instanceof LaneEditPart
                    || !(container instanceof PoolPoolCompartmentEditPart)
                    && !(container instanceof SubProcessSubProcessBodyCompartmentEditPart)) {
                editParts.clear();
                break;
            }
            if (container instanceof SubProcessSubProcessBodyCompartmentEditPart) {
                if (isInsideAnother(container, editParts)) {
                    iterator.remove();
                    continue;
                }
            }
            if (parentContainer == null) {
                parentContainer = container;
            } else if (parentContainer != container) {
                editParts.clear();
                break;
            }
        }

        for (IGraphicalEditPart editPart : editParts) {
            sortConnections(editPart, internalConnections,
                    externalSrcConnection, externalTgtConnections);
        }

        EditPart element = null;
        for (SequenceEdgeEditPart connection : externalSrcConnection) {
            EditPart currElement = connection.getSource();
            if (element == null) {
                element = currElement;
            } else if (element != currElement) {
                setEnabled(false);
                return;
            }
        }
        element = null;
        for (SequenceEdgeEditPart connection : externalTgtConnections) {
            EditPart currElement = connection.getTarget();
            if (element == null) {
                element = currElement;
            } else if (element != currElement) {
                setEnabled(false);
                return;
            }
        }

        setEnabled(!editParts.isEmpty());
    }

    /**
     * Determines whether specified edit part is a child of ony other edit part
     * (in undefined depth)
     * 
     * @param editPart
     *            the edit part to test
     * @param editParts
     *            other edit parts
     * @return <code>true</code> if specified edit part is child of ony other
     *         edit part (in undefined depth), <code>false</code> otherwise.
     */
    private static boolean isInsideAnother(EditPart editPart, List editParts) {
        SubProcessEditPart subProcess = getSubProcess(editPart);
        while (subProcess != null) {
            if (editParts.contains(subProcess)) {
                return true;
            }
            subProcess = getSubProcess(subProcess.getParent());
        }
        return false;
    }

    /**
     * Composite Group Command that uses Undo Action for undo/redo.
     * 
     * @author MPeleshchyshyn
     */
    private static class GroupCommand extends CompositeCommand {
        private ICommand ungroupCommand = null;

        private EditPart parent;

        private Object model;

        public GroupCommand(String label, SubProcessEditPart createdSubProcess) {
            super(label);
            parent = createdSubProcess.getParent();
            model = createdSubProcess.getModel();
        }

        @Override
        public IStatus undo(IProgressMonitor progressMonitor, IAdaptable info)
                throws ExecutionException {
            if (ungroupCommand == null) {
                SubProcessEditPart subProcess = null;
                for (Object childEditPart : parent.getChildren()) {
                    if (((EditPart) childEditPart).getModel() == model) {
                        subProcess = (SubProcessEditPart) childEditPart;
                        break;
                    }
                }
                assert subProcess != null;
                ungroupCommand = UngroupAction.getUngroupCommand(subProcess);
            }
            return ungroupCommand.execute(progressMonitor, info);
        }

        @Override
        public IStatus redo(IProgressMonitor progressMonitor, IAdaptable info)
                throws ExecutionException {
            return ungroupCommand.undo(progressMonitor, info);
        }
    }

    /**
     * This class fixes <code>NullPointerException</code> in case if view does
     * not have resource.
     * 
     * @author MPeleshchyshyn
     * 
     */
    private static class AddCommand extends
            org.eclipse.gmf.runtime.diagram.core.commands.AddCommand {
        private IAdaptable parent;

        public AddCommand(TransactionalEditingDomain editingDomain,
                IAdaptable parent, IAdaptable child) {
            super(editingDomain, parent, child);
            this.parent = parent;
        }

        public List getAffectedFiles() {
            View view = (View) parent.getAdapter(View.class);

            if (view != null) {
                List result = new ArrayList();
                Resource ressource = view.eResource();
                if (ressource != null) {
                    IFile file = WorkspaceSynchronizer
                            .getFile(view.eResource());

                    if (file != null) {
                        result.add(file);
                    }
                }
                return result;
            }

            return super.getAffectedFiles();
        }
    }

}