/*
 * Copyright 2015 JBoss, by Red Hat, Inc
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.uberfire.ext.wires.core.api.shapes;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import com.ait.lienzo.client.core.animation.AnimationProperties;
import com.ait.lienzo.client.core.animation.AnimationTweener;
import com.ait.lienzo.client.core.animation.IAnimation;
import com.ait.lienzo.client.core.animation.IAnimationCallback;
import com.ait.lienzo.client.core.animation.IAnimationHandle;
import com.ait.lienzo.client.core.event.NodeDragMoveEvent;
import com.ait.lienzo.client.core.event.NodeDragMoveHandler;
import com.ait.lienzo.client.core.event.NodeMouseClickEvent;
import com.ait.lienzo.client.core.event.NodeMouseClickHandler;
import com.ait.lienzo.client.core.shape.Group;
import com.ait.lienzo.client.core.shape.Layer;
import com.ait.lienzo.client.core.types.Point2D;
import org.uberfire.commons.data.Pair;
import org.uberfire.commons.uuid.UUID;
import org.uberfire.ext.wires.core.api.selection.RequiresSelectionManager;
import org.uberfire.ext.wires.core.api.selection.SelectionManager;

/**
 * A Fixed Shape that cannot be re-sized or have connectors attached
 */
public abstract class WiresBaseShape extends Group implements WiresShape,
                                                              RequiresSelectionManager {

    private static final int ANIMATION_DURATION = 250;
    private static final int DEFAULT_CONTROL_SPACING = 30;
    private static final int DEFAULT_CONTROL_POSITION_X_OFFSET = 100;
    private static final int DEFAULT_CONTROL_POSITION_Y_OFFSET = 0;
    protected String id;
    protected SelectionManager selectionManager;
    protected List<Group> controls = new ArrayList<Group>();
    protected boolean isControlsVisible = false;
    private IAnimationHandle animationHandle;

    public WiresBaseShape() {
        id = UUID.uuid();
        setDraggable(true);

        //Clicking the Group selects the Shape
        addNodeMouseClickHandler(new NodeMouseClickHandler() {
            @Override
            public void onNodeMouseClick(final NodeMouseClickEvent nodeMouseClickEvent) {
                selectionManager.selectShape(WiresBaseShape.this);
            }
        });

        //Update Control positions when Shape is dragged
        addNodeDragMoveHandler(new NodeDragMoveHandler() {
            @Override
            public void onNodeDragMove(final NodeDragMoveEvent nodeDragMoveEvent) {
                updateControlLocations();
            }
        });
    }

    @Override
    public String getId() {
        return this.id;
    }

    @Override
    public void setSelectionManager(final SelectionManager manager) {
        this.selectionManager = manager;
    }

    @Override
    public void showControls() {
        if (controls == null || controls.isEmpty()) {
            return;
        }
        if (isControlsVisible) {
            return;
        }
        if (animationHandle != null) {
            animationHandle.stop();
        }
        isControlsVisible = true;
        animationHandle = animate(AnimationTweener.EASE_OUT,
                                  new AnimationProperties(),
                                  ANIMATION_DURATION,
                                  new IAnimationCallback() {

                                      private final AnimationTweener tweener = AnimationTweener.TweenerBuilder.MAKE_ELASTIC(1);
                                      private final Map<Group, Pair<Point2D, Point2D>> transformations = new HashMap<Group, Pair<Point2D, Point2D>>();

                                      @Override
                                      public void onStart(final IAnimation iAnimation,
                                                          final IAnimationHandle iAnimationHandle) {
                                          //Store required transformations: Control, Current location, Target location
                                          transformations.clear();
                                          for (int index = 0; index < controls.size(); index++) {
                                              final Group ctrl = controls.get(index);
                                              final Point2D origin = new Point2D(0,
                                                                                 0);
                                              final Point2D target = getControlTarget(ctrl);
                                              transformations.put(ctrl,
                                                                  new Pair<Point2D, Point2D>(origin,
                                                                                             target));
                                              WiresBaseShape.this.getLayer().add(ctrl);
                                              ctrl.setLocation(origin);
                                              ctrl.setAlpha(0.0);
                                          }
                                      }

                                      @Override
                                      public void onFrame(final IAnimation iAnimation,
                                                          final IAnimationHandle iAnimationHandle) {
                                          final double pct = tweener.apply(iAnimation.getPercent() > 1.0 ? 1.0 : iAnimation.getPercent());

                                          //Move each Control along the line between its origin and the target destination
                                          for (Map.Entry<Group, Pair<Point2D, Point2D>> e : transformations.entrySet()) {
                                              final Point2D origin = e.getValue().getK1();
                                              final Point2D target = e.getValue().getK2();
                                              final double dx = (target.getX() - origin.getX()) * pct;
                                              final double dy = (target.getY() - origin.getY()) * pct;
                                              e.getKey().setLocation(new Point2D(origin.getX() + dx,
                                                                                 origin.getY() + dy).add(WiresBaseShape.this.getLocation()));
                                          }

                                          for (Group ctrl : controls) {
                                              ctrl.setAlpha(iAnimation.getPercent());
                                          }

                                          //Without this call Lienzo doesn't update the Canvas for sub-classes of WiresBaseTreeNode
                                          WiresBaseShape.this.getLayer().batch();
                                      }

                                      @Override
                                      public void onClose(final IAnimation iAnimation,
                                                          final IAnimationHandle iAnimationHandle) {
                                          //Do nothing
                                      }
                                  });
    }

    @Override
    public void hideControls() {
        if (controls == null || controls.isEmpty()) {
            return;
        }
        if (!isControlsVisible) {
            return;
        }
        if (animationHandle != null) {
            animationHandle.stop();
        }
        isControlsVisible = false;
        animationHandle = animate(AnimationTweener.EASE_OUT,
                                  new AnimationProperties(),
                                  ANIMATION_DURATION,
                                  new IAnimationCallback() {

                                      private final AnimationTweener tweener = AnimationTweener.TweenerBuilder.MAKE_EASE_IN(3.0);
                                      private final Map<Group, Pair<Point2D, Point2D>> transformations = new HashMap<Group, Pair<Point2D, Point2D>>();

                                      @Override
                                      public void onStart(final IAnimation iAnimation,
                                                          final IAnimationHandle iAnimationHandle) {
                                          //Store required transformations: Control, Current location, Target location
                                          transformations.clear();
                                          for (int index = 0; index < controls.size(); index++) {
                                              final Group ctrl = controls.get(index);
                                              final Point2D origin = ctrl.getLocation();
                                              origin.minus(WiresBaseShape.this.getLocation());
                                              final Point2D target = new Point2D(0,
                                                                                 0);
                                              transformations.put(ctrl,
                                                                  new Pair<Point2D, Point2D>(origin,
                                                                                             target));
                                          }
                                      }

                                      @Override
                                      public void onFrame(final IAnimation iAnimation,
                                                          final IAnimationHandle iAnimationHandle) {
                                          final double pct = tweener.apply(iAnimation.getPercent() > 1.0 ? 1.0 : iAnimation.getPercent());

                                          //Move each Control along the line between its origin and the target destination
                                          for (Map.Entry<Group, Pair<Point2D, Point2D>> e : transformations.entrySet()) {
                                              final Point2D origin = e.getValue().getK1();
                                              final Point2D target = e.getValue().getK2();
                                              final double dx = (target.getX() - origin.getX()) * pct;
                                              final double dy = (target.getY() - origin.getY()) * pct;
                                              e.getKey().setLocation(new Point2D(origin.getX() + dx,
                                                                                 origin.getY() + dy).add(WiresBaseShape.this.getLocation()));
                                          }

                                          for (Group ctrl : controls) {
                                              ctrl.setAlpha(1.0 - iAnimation.getPercent());
                                          }

                                          //Without this call Lienzo doesn't update the Canvas for sub-classes of WiresBaseTreeNode
                                          WiresBaseShape.this.getLayer().batch();
                                      }

                                      @Override
                                      public void onClose(final IAnimation iAnimation,
                                                          final IAnimationHandle iAnimationHandle) {
                                          for (Group ctrl : controls) {
                                              WiresBaseShape.this.getLayer().remove(ctrl);
                                          }
                                      }
                                  });
    }

    @Override
    public void addControl(final Group ctrlToAdd) {
        if (!isControlsVisible) {
            controls.add(ctrlToAdd);
            return;
        }
        final List<Group> newControls = new ArrayList<Group>(controls);
        newControls.add(ctrlToAdd);
        setControls(newControls);
    }

    @Override
    public void removeControl(final Group ctrlToRemove) {
        if (!isControlsVisible) {
            controls.remove(ctrlToRemove);
            return;
        }
        final List<Group> newControls = new ArrayList<Group>(controls);
        newControls.remove(ctrlToRemove);
        setControls(newControls);
    }

    @Override
    public void setControls(final List<Group> newControls) {
        if (!isControlsVisible) {
            controls.clear();
            controls.addAll(newControls);
            return;
        }
        if (animationHandle != null) {
            animationHandle.stop();
        }
        animationHandle = animate(AnimationTweener.EASE_OUT,
                                  new AnimationProperties(),
                                  ANIMATION_DURATION,
                                  new IAnimationCallback() {

                                      private final List<Group> controlsToAdd = new ArrayList<Group>();
                                      private final List<Group> controlsToRemove = new ArrayList<Group>();
                                      private final List<Group> controlsToRemain = new ArrayList<Group>();
                                      private final AnimationTweener tweener = AnimationTweener.TweenerBuilder.MAKE_ELASTIC(1);
                                      private final Map<Group, Pair<Point2D, Point2D>> transformations = new HashMap<Group, Pair<Point2D, Point2D>>();

                                      @Override
                                      public void onStart(final IAnimation iAnimation,
                                                          final IAnimationHandle iAnimationHandle) {
                                          //Initialise new Controls being added
                                          controlsToAdd.clear();
                                          controlsToAdd.addAll(newControls);
                                          controlsToAdd.removeAll(controls);
                                          for (Group ctrl : controlsToAdd) {
                                              ctrl.setLocation(new Point2D(0,
                                                                           0));
                                              ctrl.setAlpha(0.0);
                                              WiresBaseShape.this.getLayer().add(ctrl);
                                          }

                                          //Initialise new Controls being removed
                                          controlsToRemove.clear();
                                          controlsToRemove.addAll(controls);
                                          controlsToRemove.removeAll(newControls);

                                          //Initialise remaining Controls
                                          controlsToRemain.clear();
                                          controlsToRemain.addAll(controls);
                                          controlsToRemain.removeAll(controlsToAdd);
                                          controlsToRemain.removeAll(controlsToRemove);

                                          //Store required transformations: Control, Current location, Target location
                                          controls.clear();
                                          controls.addAll(newControls);
                                          transformations.clear();
                                          for (Group ctrl : controlsToAdd) {
                                              final Point2D origin = new Point2D(0,
                                                                                 0);
                                              final Point2D target = getControlTarget(ctrl);
                                              transformations.put(ctrl,
                                                                  new Pair<Point2D, Point2D>(origin,
                                                                                             target));
                                          }
                                          for (Group ctrl : controlsToRemove) {
                                              final Point2D origin = ctrl.getLocation();
                                              origin.minus(WiresBaseShape.this.getLocation());
                                              final Point2D target = new Point2D(0,
                                                                                 0);
                                              transformations.put(ctrl,
                                                                  new Pair<Point2D, Point2D>(origin,
                                                                                             target));
                                          }
                                          for (Group ctrl : controlsToRemain) {
                                              final Point2D origin = ctrl.getLocation();
                                              origin.minus(WiresBaseShape.this.getLocation());
                                              final Point2D target = getControlTarget(ctrl);
                                              transformations.put(ctrl,
                                                                  new Pair<Point2D, Point2D>(origin,
                                                                                             target));
                                          }
                                      }

                                      @Override
                                      public void onFrame(final IAnimation iAnimation,
                                                          final IAnimationHandle iAnimationHandle) {
                                          final double pct = tweener.apply(iAnimation.getPercent() > 1.0 ? 1.0 : iAnimation.getPercent());

                                          //Move each Control along the line between its origin and the target destination
                                          for (Map.Entry<Group, Pair<Point2D, Point2D>> e : transformations.entrySet()) {
                                              final Point2D origin = e.getValue().getK1();
                                              final Point2D target = e.getValue().getK2();
                                              final double dx = (target.getX() - origin.getX()) * pct;
                                              final double dy = (target.getY() - origin.getY()) * pct;
                                              e.getKey().setLocation(new Point2D(origin.getX() + dx,
                                                                                 origin.getY() + dy).add(WiresBaseShape.this.getLocation()));
                                          }

                                          for (Group ctrl : controlsToAdd) {
                                              ctrl.setAlpha(pct);
                                          }
                                          for (Group ctrl : controlsToRemove) {
                                              ctrl.setAlpha(1.0 - pct);
                                          }

                                          //Without this call Lienzo doesn't update the Canvas for sub-classes of WiresBaseTreeNode
                                          WiresBaseShape.this.getLayer().batch();
                                      }

                                      @Override
                                      public void onClose(final IAnimation iAnimation,
                                                          final IAnimationHandle iAnimationHandle) {
                                          isControlsVisible = !controls.isEmpty();
                                          for (Group ctrl : controlsToRemove) {
                                              WiresBaseShape.this.getLayer().remove(ctrl);
                                          }
                                      }
                                  });
    }

    @Override
    public boolean isControlsVisible() {
        return this.isControlsVisible;
    }

    /**
     * Returns a Point (relative to the WiresShape) where a Control should be placed when Controls
     * are shown. This default implementation places all Controls at WiresShape.getX()+100 and evenly
     * spaces the Controls vertically with centres spaced 30px
     * @param ctrl The Control to position
     * @return The position of the Control
     */
    protected Point2D getControlTarget(final Group ctrl) {
        final int offsetY = -((controls.size() - 1) * DEFAULT_CONTROL_SPACING) / 2;
        final Point2D target = new Point2D(DEFAULT_CONTROL_POSITION_X_OFFSET,
                                           DEFAULT_CONTROL_POSITION_Y_OFFSET + offsetY + (controls.indexOf(ctrl) * DEFAULT_CONTROL_SPACING));
        return target;
    }

    @Override
    public void destroy() {
        if (isControlsVisible) {
            for (Group ctrl : controls) {
                getLayer().remove(ctrl);
            }
            isControlsVisible = false;
        }
        Layer layer = getLayer();
        layer.remove(this);
        layer.batch();
    }

    //Move the Controls to match where the descendant has been moved
    private void updateControlLocations() {
        if (controls == null) {
            return;
        }
        if (!isControlsVisible) {
            return;
        }
        for (Group ctrl : controls) {
            final Point2D target = getControlTarget(ctrl).add(WiresBaseShape.this.getLocation());
            ctrl.setLocation(target);
        }
    }

    //Move the Controls to match where the descendant has been moved
    private void updateControlLocations(final double dx,
                                        final double dy) {
        if (controls == null) {
            return;
        }
        if (!isControlsVisible) {
            return;
        }
        for (Group ctrl : controls) {
            ctrl.setLocation(ctrl.getLocation().add(new Point2D(dx,
                                                                dy)));
        }
    }

    @Override
    public Group setX(final double x) {
        final double dx = x - super.getX();
        final Group g = super.setX(x);
        updateControlLocations(dx,
                               0);
        return g;
    }

    @Override
    public Group setY(final double y) {
        final double dy = y - super.getY();
        final Group g = super.setY(y);
        updateControlLocations(0,
                               dy);
        return g;
    }
}
