/******************************************************************************
 * 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
 * Dec 06, 2006     hmalphettes         Created
 **/
package org.eclipse.stp.bpmn.policies;

import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.SortedSet;
import java.util.TreeSet;

import org.eclipse.draw2d.geometry.Insets;
import org.eclipse.draw2d.geometry.Point;
import org.eclipse.draw2d.geometry.Rectangle;
import org.eclipse.emf.transaction.TransactionalEditingDomain;
import org.eclipse.gef.commands.Command;
import org.eclipse.gef.commands.CompoundCommand;
import org.eclipse.gef.requests.ChangeBoundsRequest;
import org.eclipse.gef.requests.CreateRequest;
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.editpolicies.XYLayoutEditPolicy;
import org.eclipse.gmf.runtime.diagram.ui.l10n.DiagramUIMessages;
import org.eclipse.gmf.runtime.diagram.ui.requests.CreateViewRequest;
import org.eclipse.gmf.runtime.diagram.ui.requests.CreateViewRequest.ViewDescriptor;
import org.eclipse.gmf.runtime.emf.commands.core.command.CompositeTransactionalCommand;
import org.eclipse.gmf.runtime.notation.NotationPackage;
import org.eclipse.stp.bpmn.diagram.edit.parts.LaneEditPart;
import org.eclipse.stp.bpmn.diagram.edit.parts.PoolEditPart;
import org.eclipse.stp.bpmn.diagram.edit.parts.PoolPoolCompartmentEditPart;


/**
 * The edit policy makes sure the lanes are correctly laid out.
 * <p>
 * Derived from the BpmnDiagramCompartmentXYLayoutEditPolicy by MPeleshchyshyn.
 * </p>
 * TODO: also change the location of the shapes inside tha lanes when the lanes
 * are resized.
 *  
 * @author hmalphettes
 * @author <a href="http://www.intalio.com">&copy; Intalio, Inc.</a>
 */
public class PoolPoolCompartmentXYLayoutEditPolicy extends XYLayoutEditPolicy {

    /**
     * @notgenerated
     */
    public static final Insets INSETS = new Insets(5, 6, 5, 5);

    /*
     * (non-Javadoc)
     * 
     * @see org.eclipse.gef.editpolicies.ConstrainedLayoutEditPolicy#getResizeChildrenCommand(org.eclipse.gef.requests.ChangeBoundsRequest)
     */
    protected Command getResizeChildrenCommand(ChangeBoundsRequest request) {
        CompoundCommand resize = new CompoundCommand();
        
        Map<IGraphicalEditPart, Rectangle> map = new HashMap<IGraphicalEditPart, Rectangle>();
        SortedSet<Rectangle> set = new TreeSet<Rectangle>(new RectanglesComparator());
        
        fillMapAndSet(map, set, request, resize);

        if (map.isEmpty()) {
            return super.getResizeChildrenCommand(request);
        }
        
        doLayout(request, map, set, resize, null);

        return resize.unwrap();
    }

    /**
     * Indexes the various lane edit part and their bounds.
     * 
     * @param map
     *            edit part - bounds map
     * @param orderedLaneBounds
     *            sorted bounds set (sorts rectangles by their coordinate)
     * @param request
     *            change bouds request (can be <code>null</code> for create
     *            command)
     * @param cc
     *            compound command to append set bounds command for annotations.
     *            Can be <code>null</code> for create request.
     * @return maximal poool width inside diagram.
     */
    private void fillMapAndSet(
            Map<IGraphicalEditPart, Rectangle> map,
            SortedSet<Rectangle> orderedLaneBounds,
            ChangeBoundsRequest request, CompoundCommand cc) {
        List children = request != null ? request.getEditParts()
                : Collections.EMPTY_LIST;
        List allChildren = getHost().getChildren();

        Iterator iter = allChildren.iterator();
        while (iter.hasNext()) {
            IGraphicalEditPart child = (IGraphicalEditPart) iter.next();
            int x = 0;
            if (child instanceof LaneEditPart) {
                Rectangle bounds;
                if (children.contains(child)) {
                    bounds = (Rectangle) translateToModelConstraint(getConstraintFor(
                            request, child));
                    bounds.x = x;
                } else {
                    int y = ((Integer) child
                            .getStructuralFeatureValue(NotationPackage.eINSTANCE
                                    .getLocation_Y())).intValue();
                    int width = ((Integer) child
                            .getStructuralFeatureValue(NotationPackage.eINSTANCE
                                    .getSize_Width())).intValue();
                    int height = ((Integer) child
                            .getStructuralFeatureValue(NotationPackage.eINSTANCE
                                    .getSize_Height())).intValue();
                    bounds = new Rectangle(x, y, width, height);
                }

                map.put(child, bounds);
                orderedLaneBounds.add(bounds);
            } else if (children.contains(child)) {
                Command c = createChangeConstraintCommand(request, child,
                        translateToModelConstraint(getConstraintFor(request,
                                child)));
                cc.add(c);
            } else {
                
            }
        }
    }
    
    /**
     * Simple comparator class for Rectangles comparison
     * 
     * @author MPeleshchyshyn
     * @author <a href="http://www.intalio.com">&copy; Intalio, Inc.</a>
     */
    private class RectanglesComparator implements Comparator<Rectangle> {

        /**
         * Rectangular A is bigger then rectangular B if Y coordinate of A is
         * bigger then Y coordinate of b
         */
        public int compare(Rectangle r1, Rectangle r2) {
            if (r1.y > r2.y) {
                return 1;
            } else if (r1.y < r2.y) {
                return -1;
            }
            return 0;
        }
    }

    
    /**
     * Lays out lanes, sets pools' width, creates
     * <code>SetBoundsCommand</code>s and appends them to the specified
     * compound command
     * 
     * @param map
     *            edit part - bounds map
     * @param set
     *            sorted bounds set - holds all lanes' bounds ordered by y
     *            coordinate
     * @param cc
     *            compound command to append create set bounds commands
     * @param insertedRectangles
     *            The set of rectangles that don't have an edit part yet
     *            as they correspond to the bounds of created objects
     */
    private void doLayout(ChangeBoundsRequest request,
            Map<IGraphicalEditPart, Rectangle> map,
            SortedSet<Rectangle> set, CompoundCommand cc,
            Set<Rectangle> insertedRectangles) {

        //see if the BPmnDiagramLayout edit policy called us.
        //we check if the container is the edit part being modifid.
        //if it is the case we either shorten the top lane or the bottom lane.
        //by the size difference.
        if (request != null && request.getEditParts() != null &&
                request.getEditParts().size() == 1 && 
                request.getEditParts().get(0) instanceof PoolEditPart) {
            //the pool edit part that contains this compartment is being resized.
            //we need to resize either the top-most lane or the bottom most lane.
            PoolEditPart container = (PoolEditPart)request.getEditParts().get(0);
            if (container == getHost().getParent() &&
                    request.getSizeDelta().height != 0) {
                Rectangle r = null;
                if (request.getMoveDelta().y != 0) {
                    //then it is the top line of the pool that is moved
                    //and causes the resize
                    //resize the first lane:
                    Iterator<Rectangle> it = set.iterator();
                    r = it.next();
                    r.setSize(r.width, r.height + request.getSizeDelta().height);
                    if (it.hasNext()) {
                        Rectangle second = it.next();
                        second.y = r.y + r.height;
                    }
                } else {
                    //it is the last
                    r = set.last();
                    r.setSize(r.width, r.height + request.getSizeDelta().height);
                }
            } else {
                return;
            }
        } else if (request != null) {
            
            //decide if the resize is happening at the bottom of a lane or at the top
            if (request.getMoveDelta().y != 0 &&
                    request.getSizeDelta().height == -request.getMoveDelta().y) {
                //it is the resize of the top of a lane
                LinkedList<Rectangle> rects = new LinkedList<Rectangle>(set);
                Rectangle prevLast = null;//rects.removeLast();
                boolean foundTheFirst = false;
                int beforeChangeTopY = -1;
                int beforeChangeBottomY = -1;
                while (!rects.isEmpty()) {
                    Rectangle last = rects.removeLast();
                    if (prevLast != null) {
                        if (prevLast.y != last.y + last.height) {
                            beforeChangeTopY = last.y;
                            beforeChangeBottomY = last.y + last.height;
                            last.height = prevLast.y - last.y;
                            if (last.height <= 48) {
                                last.height = 48;
                                int prevBottomY = prevLast.y + prevLast.height;
                                prevLast.y = last.y + last.height;
                                prevLast.height = prevBottomY - prevLast.y;
                            }
                            
                            //the following is dumb: 2 editparts are actually updated.
                            //not one.
                            /*
                            if (true) {
                                //found the first lane.
                                //it is the lane being resized.
                                //let's compute the the coeff of the resize.
                                //then let's move all the shapes inside the lane
                                //to keep them inside the lane
                                foundTheFirst = true;
                                for (EditPart child : (List<EditPart>)getHost().getChildren()) {
                                    if (child instanceof ShapeEditPart && !(child instanceof LaneEditPart)) {
                                        ShapeEditPart se = (ShapeEditPart)child;
                                        Point topLeft = se.getFigure().getBounds().getTopLeft();
                                        System.err.println(beforeChangeBottomY + " > " + topLeft.y + " >= " + beforeChangeTopY);
                                        if (topLeft.y >= beforeChangeTopY &&
                                                topLeft.y < beforeChangeBottomY) {
                                            System.err.println("got one");
                                            Rectangle newB =
                                                se.getFigure().getBounds().getCopy();
                                            
                                            int beforeLaneHeight = beforeChangeBottomY - beforeChangeTopY;
                                            int afterLaneHeight = last.height;
                                            double coeff = 1.0*afterLaneHeight/beforeLaneHeight;
                                            
                                            int diffChildToBottom = -beforeChangeBottomY + topLeft.y;
                                            double newDiff = diffChildToBottom * coeff;
                                            
                                            newB.y = topLeft.y + (int)Math.round(newDiff);
                                            Command c = createChangeConstraintCommand(se, newB);
                                            cc.add(c);
                                        }
                                    }
                                }
                                
                            }*/
                            
                        }
                    }
                    prevLast = last;
                }
            } else {
                //resize happening at the bottom.

                // prevent lanes overlapping
                Rectangle firstBounds = null;
                for (Rectangle secondBounds : set) {
                    if ((firstBounds != null &&
                            firstBounds.y + firstBounds.height != secondBounds.y) ||
                            secondBounds.y != INSETS.top){
        //                int yDelta = firstBounds != null ?
        //                    firstBounds.y + firstBounds.height - secondBounds.y :
        //                        INSETS.top - secondBounds.y;
                        int heightDelta = -secondBounds.y;
                        if (firstBounds == null) {
                            secondBounds.setLocation(0, INSETS.top);
                        } else {
                            secondBounds.setLocation(0, firstBounds.y + firstBounds.height);
                        }
                        heightDelta += secondBounds.y;
                        //reduce the height of the lane to accomodate the change:
                        secondBounds.setSize(secondBounds.width, secondBounds.height - heightDelta);
                    }
                    firstBounds = secondBounds;
                }
                //make sure the last entry actually fills the rest of the room:
                //make sure the last one is updated to be at the border of the pool:
                Point bottomLane = firstBounds.getBottom();
                Point bottomPool = getHostFigure().getBounds().getBottom();
                
                if (bottomLane.y != bottomPool.y - INSETS.bottom) {
                    firstBounds.setSize(firstBounds.width, firstBounds.height
                            + bottomPool.y - bottomLane.y - INSETS.bottom);
                }
        
            }
        }

        Set<Map.Entry<IGraphicalEditPart, Rectangle>> entries = map.entrySet();
        Iterator<Map.Entry<IGraphicalEditPart, Rectangle>> outerIterator = entries
                .iterator();
        while (outerIterator.hasNext()) {
            Map.Entry<IGraphicalEditPart, Rectangle> outerEntry = outerIterator
                    .next();
            IGraphicalEditPart pep = outerEntry.getKey();
            Rectangle rect = outerEntry.getValue();

            Rectangle prevBounds = pep.getFigure().getBounds();
            if (!prevBounds.equals(rect)) {
                Command c = createChangeConstraintCommand(pep, rect);
                cc.add(c);
            }
        }
    }

    @Override
    protected Command getCreateCommand(CreateRequest request) {
        CreateViewRequest req = (CreateViewRequest) request;

        TransactionalEditingDomain editingDomain = ((IGraphicalEditPart) getHost())
                .getEditingDomain();

        CompositeTransactionalCommand cc = new CompositeTransactionalCommand(
                editingDomain, DiagramUIMessages.AddCommand_Label);
        Iterator iter = req.getViewDescriptors().iterator();

        final Rectangle BOUNDS = (Rectangle) getConstraintFor(request);

        Map<IGraphicalEditPart, Rectangle> map = new HashMap<IGraphicalEditPart, Rectangle>();
        SortedSet<Rectangle> set = new TreeSet<Rectangle>(
                new RectanglesComparator());
        fillMapAndSet(map, set, null, null);
        Map<ViewDescriptor, Rectangle> descriptorsMap = new HashMap<ViewDescriptor, Rectangle>();
        Set<Rectangle> insertedRectangles = null;
        while (iter.hasNext()) {
            CreateViewRequest.ViewDescriptor viewDescriptor = (CreateViewRequest.ViewDescriptor) iter
                    .next();
            Rectangle rect = getBoundsOffest(req, BOUNDS, viewDescriptor);
            if (viewDescriptor.getSemanticHint().equals(
                    String.valueOf(LaneEditPart.VISUAL_ID))) {
                rect.x = 0;
                //divide by 2 the height of the lane where
                //the new lane is created.
                //use the new height as the height of the lane.
                Point location = req.getLocation().getCopy();
                getHostFigure().translateToRelative(location);
                int ind = 0;
                for (Rectangle otherLane : set) {
//                    System.err.println(otherLane.y + " compared " + location.y
//                            + " upper " + otherLane.y + otherLane.height);
                    if (otherLane.y < location.y && otherLane.y + otherLane.height >= location.y) {
                        //ok we got the lane
                        otherLane.height = otherLane.height / 2;
                        rect.height = otherLane.height;
                        //see if we insert it before or after the lane
                        //where the creation request is made.
                        //if in the lower half we insert it after.
                        //if in the top half we insert it before.
                        if (otherLane.y + otherLane.height >= location.y) {
                            rect.y = otherLane.y;
//                          add +2 to make sure a command will be issued.
                            otherLane.y += rect.height + 2;
                        } else {
                            rect.y = otherLane.y + rect.height + 2;
                        }
                        break;
                    }
                    ind++;
                }
                
                if (insertedRectangles == null) {
                    insertedRectangles = new HashSet<Rectangle>();
                }
                if (ind == set.size()) {
                    //it is the first lane in a pool.
                    //make sure it will take the entire height:
                    rect.y = PoolPoolCompartmentEditPart.INSETS.top;
//                    PoolPoolCompartmentEditPart comp =
//                        (PoolPoolCompartmentEditPart)getHost();
                    rect.height = getHostFigure().getBounds().height
//                        ((PoolEditPart)comp.getParent()).getSize().height
                        - PoolPoolCompartmentEditPart.INSETS.top
                        - PoolPoolCompartmentEditPart.INSETS.bottom;
                }
                insertedRectangles.add(rect);                
            }
            descriptorsMap.put(viewDescriptor, rect);
        }
        if (insertedRectangles != null) {
            //now add the rectangles:
            set.addAll(insertedRectangles);
        }

        CompoundCommand compoundCommand = new CompoundCommand();
        doLayout(null, map, set, compoundCommand, insertedRectangles);
        if (compoundCommand.canExecute()) {
            cc.compose(new CommandProxy(compoundCommand));
        }
        Iterator<Map.Entry<ViewDescriptor, Rectangle>> iterator = descriptorsMap
                .entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry<ViewDescriptor, Rectangle> entry = iterator.next();
            cc.compose(new SetBoundsCommand(editingDomain,
                    DiagramUIMessages.SetLocationCommand_Label_Resize, entry
                            .getKey(), entry.getValue()));
        }

        if (cc.reduce() == null)
            return null;

        return chainGuideAttachmentCommands(request, new ICommandProxy(cc
                .reduce()));
    }    
    
}
