/******************************************************************************
 * 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 
 * 14 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.runtime.IProgressMonitor;
import org.eclipse.draw2d.IFigure;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.emf.ecore.EObject;
import org.eclipse.emf.transaction.TransactionalEditingDomain;
import org.eclipse.gef.ConnectionEditPart;
import org.eclipse.gef.EditPart;
import org.eclipse.gef.tools.ToolUtilities;
import org.eclipse.gmf.runtime.common.core.command.CompositeCommand;
import org.eclipse.gmf.runtime.common.core.command.ICommand;
import org.eclipse.gmf.runtime.common.core.command.UnexecutableCommand;
import org.eclipse.gmf.runtime.diagram.core.commands.AddCommand;
import org.eclipse.gmf.runtime.diagram.core.commands.DeleteCommand;
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.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.emf.core.util.EObjectAdapter;
import org.eclipse.gmf.runtime.emf.type.core.commands.DestroyElementCommand;
import org.eclipse.gmf.runtime.emf.type.core.commands.MoveElementsCommand;
import org.eclipse.gmf.runtime.emf.type.core.requests.DestroyElementRequest;
import org.eclipse.gmf.runtime.emf.type.core.requests.MoveRequest;
import org.eclipse.gmf.runtime.notation.View;
import org.eclipse.jface.action.IAction;
import org.eclipse.jface.viewers.IStructuredSelection;
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.ui.IWorkbenchPage;
import org.eclipse.ui.IWorkbenchPart;

/**
 * This class implements functionality that moves tasks outside sub-process.
 * Subpocess itself is removed.
 * 
 * @author MPeleshchyshyn
 * @author <a href="http://www.intalio.com">&copy; Intalio, Inc.</a>
 */
public class UngroupAction extends AbstractGroupUngroupAction {
    public static final String ACTION_ID = "ungroupAction";

    public static final String TOOLBAR_ACTION_ID = "toolbarUngroupAction";

    private static final String ICON_PATH = "icons/Ungroup.gif";

    private static final String ACTION_TEXT = "Ungroup";

    private static final String TOOLTIP_TEXT = "Ungroup Shapes(Remove Sub-Process)";

    /**
     * Holds processes to which children should be moved outside.
     */
    private List<SubProcessEditPart> subProcesses = new ArrayList<SubProcessEditPart>();

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

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

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

        return action;
    }

    public static UngroupAction createUngroupAction(IWorkbenchPage workbenchPage) {
        UngroupAction action = createActionWithoutId(workbenchPage);
        action.setId(ACTION_ID);

        return action;
    }

    public static UngroupAction createToolbarUngroupAction(
            IWorkbenchPage workbenchPage) {
        UngroupAction action = createActionWithoutId(workbenchPage);
        action.setId(TOOLBAR_ACTION_ID);

        return action;
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.eclipse.ui.IActionDelegate#run(org.eclipse.jface.action.IAction)
     */
    @Override
    protected void doRun(IProgressMonitor progressMonitor) {
        CompositeCommand cc = new CompositeCommand("Ungroup");
        for (SubProcessEditPart subProcess : subProcesses) {
            cc.compose(getUngroupCommand(subProcess));
        }

        // try {
        SubProcessEditPart first = subProcesses.get(0);
        first.getDiagramEditDomain().getDiagramCommandStack().execute(
                new ICommandProxy(cc));
        // } catch (ExecutionException e) {
        // // TODO Auto-generated catch block
        // e.printStackTrace();
        // }
    }

    /**
     * Creates command that ungroups all child objects from the specified
     * subprocess. This composite command includes semantic and visual
     * reparenting of all child objects, incoming and outgoing connections
     * reparenting and semantic reparenting of internal connections (those
     * between child objects) and finally sub-process removal commands.
     * 
     * @param subProcessEditPart
     *            the sub-process edit part whch children should be ungrouped.
     * @return composite command that performs all necessary ungoup operations
     *         for the specified sub-process
     */
    public static ICommand getUngroupCommand(
            SubProcessEditPart subProcessEditPart) {
        List childrenToUngroup = subProcessEditPart
                .getChildBySemanticHint(
                        Integer
                                .toString(SubProcessSubProcessBodyCompartmentEditPart.VISUAL_ID))
                .getChildren();

        if (childrenToUngroup.isEmpty()) {
            // nothing to do if there are no children inside
            return UnexecutableCommand.INSTANCE;
        }

        CompositeCommand cc = new CompositeCommand(
                "Ungroup commands for signle sub-process");

        EditPart containerEditPart = subProcessEditPart.getParent();
        View container = (View) containerEditPart.getModel();
        EObject context = ViewUtil.resolveSemanticElement(container);

        ToggleCanonicalModeCommand containerCanonicalModeCommand = new ToggleCanonicalModeCommand(
                containerEditPart, false);
        cc.compose(new CommandProxy(containerCanonicalModeCommand));
        ToggleCanonicalModeCommand subProcessCanonicalModeCommand = new ToggleCanonicalModeCommand(
                subProcessEditPart, false);
        cc.compose(new CommandProxy(subProcessCanonicalModeCommand));
        // add children reparent commands
        for (Object object : childrenToUngroup) {
            IGraphicalEditPart gep = (IGraphicalEditPart) object;
            cc.compose(getReparentUngroupCommand(subProcessEditPart, gep));
        }

        // find all inner connections
        Collection<SequenceEdgeEditPart> innerConnections = new HashSet<SequenceEdgeEditPart>();
        for (Object part : childrenToUngroup) {
            IGraphicalEditPart gep = (IGraphicalEditPart) part;
            List connections = gep.getSourceConnections();
            for (Object connection : connections) {
                if (connection instanceof SequenceEdgeEditPart) {
                    innerConnections.add((SequenceEdgeEditPart) connection);
                }
            }
            connections = gep.getTargetConnections();
            for (Object connection : connections) {
                if (connection instanceof SequenceEdgeEditPart) {
                    innerConnections.add((SequenceEdgeEditPart) connection);
                }
            }
        }

        // now reparent all inner connections
        reparentInnnerConnections(innerConnections, subProcessEditPart
                .getEditingDomain(), context, cc);

        // find possible start and end children (probably we'll reparent
        // connections to them)
        List<IGraphicalEditPart> startElements = new ArrayList<IGraphicalEditPart>();
        List<IGraphicalEditPart> endElements = new ArrayList<IGraphicalEditPart>();
        getStartAndEndTasks(subProcessEditPart, startElements, endElements);
        if (startElements.size() == 1) {
            // reparent outgoing connections only in case if there is only one
            // possible candidate
            List connections = subProcessEditPart.getSourceConnections();
            int idx = 0;
            int size = connections.size();
            for (Object object : connections) {
                ConnectionEditPart connection = (ConnectionEditPart) object;
                cc.compose(getReconnectSourceCommand(connection,
                        (ShapeNodeEditPart) startElements.get(0), idx, size));
                idx++;
            }
        }
        if (endElements.size() == 1) {
            // reparent incoming connections only in case if the is only one
            // possible candidate
            List connections = subProcessEditPart.getTargetConnections();
            int idx = 0;
            int size = connections.size();
            for (Object object : connections) {
                ConnectionEditPart connection = (ConnectionEditPart) object;
                cc.compose(getReconnectTargetCommand(connection,
                        (ShapeNodeEditPart) endElements.get(0), idx, size));
                idx++;
            }
        }

        // now remove subprocess itself
        View model = (View) subProcessEditPart.getModel();
        EObject elementToDestroy = ViewUtil.resolveSemanticElement(model);
        DestroyElementRequest destroyRequest = new DestroyElementRequest(
                subProcessEditPart.getEditingDomain(), elementToDestroy, false);
        DestroyElementCommand destoryCommand = new DestroyElementCommand(
                destroyRequest);
        // enable canonical mode first for Undo/Redo functionality
        cc.compose(new CommandProxy(ToggleCanonicalModeCommand
                .getToggleCanonicalModeCommand(subProcessCanonicalModeCommand,
                        true)));
        DeleteCommand deleteViewCommand = new DeleteCommand(subProcessEditPart
                .getEditingDomain(), model);
        cc.compose(deleteViewCommand);
        cc.compose(destoryCommand);

        cc.compose(new CommandProxy(ToggleCanonicalModeCommand
                .getToggleCanonicalModeCommand(containerCanonicalModeCommand,
                        true)));
        return cc;
    }

    /**
     * Creates command that will reparent(semantically and visually) the
     * specified edit part from the specified sub-process.
     * 
     * @param subProcessEditPart
     *            parent subprocess that contains the specified edit part
     * @param childEditPart
     *            the child to be reparented
     * @return composite command the will reparent the specified edit part from
     *         the specified sub-process to sub-process' parent semantically and
     *         visually.
     */
    private static ICommand getReparentUngroupCommand(
            SubProcessEditPart subProcessEditPart,
            IGraphicalEditPart childEditPart) {
        CompositeCommand cc = new CompositeCommand(
                "Ungroup commands for single child edit part");
        EditPart containerEditPart = subProcessEditPart.getParent();
        View container = (View) containerEditPart.getModel();
        EObject context = ViewUtil.resolveSemanticElement(container);

        View view = (View) childEditPart.getModel();
        EObject element = ViewUtil.resolveSemanticElement(view);

        TransactionalEditingDomain editingDomain = childEditPart
                .getEditingDomain();

        // semantic
        if (element != null) {
            ICommand moveSemanticCmd = new MoveElementsCommand(new MoveRequest(
                    editingDomain, context, element));

            if (moveSemanticCmd == null) {
                return UnexecutableCommand.INSTANCE;
            }

            // notation
            cc
                    .compose(getReparentViewCommand(childEditPart,
                            containerEditPart));

            cc.compose(moveSemanticCmd);

            // set correct bounds
            IFigure figure = childEditPart.getFigure();
            Rectangle newBounds = figure.getBounds();
            figure.translateToAbsolute(newBounds);
            IGraphicalEditPart parentContainerEditPart = (IGraphicalEditPart) childEditPart
                    .getParent().getParent();
            parentContainerEditPart.getFigure().translateToRelative(newBounds);

            ICommand setBounsCommand = new SetBoundsCommand(childEditPart
                    .getEditingDomain(), "Update bounds", childEditPart,
                    newBounds);
            cc.compose(setBounsCommand);
        }
        return cc;
    }

    /**
     * Creates view reparent command.
     * 
     * @param childEditPart
     *            child edit part to be reparented
     * @param newContainerEditPart
     *            the new container of the specified child edit part.
     * @return view reparent command.
     */
    private static ICommand getReparentViewCommand(
            IGraphicalEditPart childEditPart, EditPart newContainerEditPart) {
        View container = (View) newContainerEditPart.getModel();
        View view = (View) childEditPart.getModel();
        return new AddCommand(childEditPart.getEditingDomain(),
                new EObjectAdapter(container), new EObjectAdapter(view));
    }

    /**
     * Searches possible end (without target sequence connections) and start
     * (without source sequence connections) elements inside the specified
     * sub-process' children. Found edit parts are added to the the specified
     * startElements and endElements lists (should be passed from the calling
     * method).
     * 
     * @param subProcessEditPart
     *            the subprocess edit part
     * @param startElements
     *            holds elements without source sequence connections
     * @param endElements
     *            holds elements without target sequence connections
     */
    private static void getStartAndEndTasks(
            SubProcessEditPart subProcessEditPart,
            List<IGraphicalEditPart> startElements,
            List<IGraphicalEditPart> endElements) {
        List children = subProcessEditPart
                .getChildBySemanticHint(
                        Integer
                                .toString(SubProcessSubProcessBodyCompartmentEditPart.VISUAL_ID))
                .getChildren();
        for (Object object : children) {
            IGraphicalEditPart editPart = (IGraphicalEditPart) object;
            List sourceConnections = editPart.getSourceConnections();
            boolean noConnections = true;
            if (!sourceConnections.isEmpty()) {
                for (Object connection : sourceConnections) {
                    if (connection instanceof SequenceEdgeEditPart) {
                        // incoming sequence connection exists so this is not
                        // start task
                        noConnections = false;
                        break;
                    }
                }
            }
            if (noConnections) {
                startElements.add(editPart);
            }
            // now test for outgoing connections
            List targetConnections = editPart.getTargetConnections();
            noConnections = true;
            if (!targetConnections.isEmpty()) {
                for (Object connection : targetConnections) {
                    if (connection instanceof SequenceEdgeEditPart) {
                        // outgoing sequence connection exists so this is not
                        // end task
                        noConnections = false;
                        break;
                    }
                }
            }
            if (noConnections) {
                endElements.add(editPart);
            }
        }
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.eclipse.ui.IActionDelegate#selectionChanged(org.eclipse.jface.action.IAction,
     *      org.eclipse.jface.viewers.ISelection)
     */
    public void refresh() {
        IStructuredSelection structuredSelection = getStructuredSelection();
        subProcesses.clear();
        if (structuredSelection != null) {
            Iterator iter = structuredSelection.iterator();
            while (iter.hasNext()) {
                Object selElement = iter.next();
                if (selElement instanceof ShapeNodeEditPart) {
                    EditPart editPart = (EditPart) selElement;
                    // select parent subprocess (or object itselt if it is
                    // subprocess)
                    SubProcessEditPart subProcess = getSubProcess(editPart);
                    if (subProcess != null
                            && !subProcesses.contains(subProcess)) {
                        subProcesses.add(subProcess);
                    }
                }
            }
            // now remove enclosed subprocesses
            subProcesses = ToolUtilities
                    .getSelectionWithoutDependants(subProcesses);
        }
        setEnabled(!subProcesses.isEmpty());
    }

    /*
     * (non-Javadoc)
     * 
     * @see org.eclipse.ui.IObjectActionDelegate#setActivePart(org.eclipse.jface.action.IAction,
     *      org.eclipse.ui.IWorkbenchPart)
     */
    public void setActivePart(IAction action, IWorkbenchPart targetPart) {
        // TODO Auto-generated method stub
    }
}
