/*
 * Copyright (c) 2008-2011, Piccolo2D project, http://piccolo2d.org
 * Copyright (c) 1998-2008, University of Maryland
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without modification, are permitted provided
 * that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this list of conditions
 * and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice, this list of conditions
 * and the following disclaimer in the documentation and/or other materials provided with the
 * distribution.
 *
 * None of the name of the University of Maryland, the name of the Piccolo2D project, or the names of its
 * contributors may be used to endorse or promote products derived from this software without specific
 * prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED
 * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
 * PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
 * TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */
package edu.umd.cs.piccolox.pswing;

import java.awt.Component;
import java.awt.Container;
import java.awt.Point;
import java.awt.event.InputEvent;
import java.awt.event.MouseEvent;
import java.awt.event.MouseWheelEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;

import javax.swing.SwingUtilities;

import edu.umd.cs.piccolo.PCamera;
import edu.umd.cs.piccolo.PLayer;
import edu.umd.cs.piccolo.PNode;
import edu.umd.cs.piccolo.event.PInputEvent;
import edu.umd.cs.piccolo.event.PInputEventListener;
import edu.umd.cs.piccolo.util.PAffineTransform;
import edu.umd.cs.piccolo.util.PAffineTransformException;

/**
 * Event handler to send MousePressed, MouseReleased, MouseMoved, MouseClicked,
 * and MouseDragged events on Swing components within a PCanvas.
 * 
 * @author Ben Bederson
 * @author Lance Good
 * @author Sam Reid
 */
public class PSwingEventHandler implements PInputEventListener {
    /** Used to listen for events. */
    private PNode listenNode = null;

    /** Tracks whether this event handler is active. */
    private boolean active = false;

    /**
     * The previous component - used to generate mouseEntered and mouseExited
     * events.
     */
    private Component previousComponent = null;

    /** Previous point used for mouseEntered and exited events. */
    private Point2D prevPoint = null;

    /** Previous offset used for mouseEntered and exited events. */
    private Point2D previousOffset = null;

    /** Used to avoid accidental recursive handling. */
    private boolean recursing = false;

    /** Used for tracking the left button's state. */
    private final ButtonData leftButtonData = new ButtonData();

    /** Used for tracking the middle button's state. */
    private final ButtonData middleButtonData = new ButtonData();

    /** Used for tracking the right button's state. */
    private final ButtonData rightButtonData = new ButtonData();

    /** The Canvas in which all this pswing activity is taking place. */
    private final PSwingCanvas canvas;

    /**
     * Constructs a new PSwingEventHandler for the given canvas, and a node that
     * will receive the mouse events.
     * 
     * @param canvas the canvas associated with this PSwingEventHandler.
     * @param listenNode the node the mouse listeners will be attached to.
     */
    public PSwingEventHandler(final PSwingCanvas canvas, final PNode listenNode) {
        this.canvas = canvas;
        this.listenNode = listenNode;
    }

    /**
     * Constructs a new PSwingEventHandler for the given canvas.
     * 
     * @param canvas to associate this event handler to
     */
    public PSwingEventHandler(final PSwingCanvas canvas) {
        this.canvas = canvas;
    }

    /**
     * Sets whether this event handler can fire events.
     * 
     * @param active true if this event handler can fire events
     */
    void setActive(final boolean active) {
        if (this.active && !active) {
            if (listenNode != null) {
                this.active = false;
                listenNode.removeInputEventListener(this);
            }
        }
        else if (!this.active && active && listenNode != null) {
            this.active = true;
            listenNode.addInputEventListener(this);
        }
    }

    /**
     * Returns if this event handler is active.
     * 
     * @return true if can fire events
     */
    public boolean isActive() {
        return active;
    }

    /**
     * Finds the best visible component or subcomponent at the specified
     * location.
     * 
     * @param component component to test children or self for
     * @param x x component of location
     * @param y y component of location
     * @return the component or subcomponent at the specified location.
     */
    private Component findShowingComponentAt(final Component component, final int x, final int y) {
        if (!component.contains(x, y)) {
            return null;
        }

        if (component instanceof Container) {
            final Container contain = (Container) component;
            final Component child = findShowingChildAt(contain, x, y);
            if (child != null) {
                return child;
            }
        }
        return component;
    }

    private Component findShowingChildAt(final Container container, final int x, final int y) {
        final Component[] children = container.getComponents();

        for (int i = 0; i < children.length; i++) {
            Component child = children[i];
            if (child != null) {
                final Point p = child.getLocation();
                if (child instanceof Container) {
                    child = findShowingComponentAt(child, x - p.x, y - p.y);
                }
                else {
                    child = child.getComponentAt(x - p.x, y - p.y);
                }
                if (child != null && child.isShowing()) {
                    return child;
                }
            }
        }

        return null;
    }

    /**
     * Determines if any Swing components in Piccolo2D should receive the given
     * MouseEvent and forwards the event to that component. However,
     * mouseEntered and mouseExited are independent of the buttons. Also, notice
     * the notes on mouseEntered and mouseExited.
     * 
     * @param pSwingMouseEvent event being dispatched
     * @param aEvent Piccolo2D event translation of the pSwingMouseEvent
     */
    void dispatchEvent(final PSwingEvent pSwingMouseEvent, final PInputEvent aEvent) {
        final MouseEvent mEvent = pSwingMouseEvent.asMouseEvent();
        final PNode pickedNode = pSwingMouseEvent.getPath().getPickedNode();
        final PNode currentNode = pSwingMouseEvent.getCurrentNode();

        Component comp = null;
        Point point = null;

        Point offset = new Point();

        if (currentNode instanceof PSwing && pickedNode.isDescendentOf(canvas.getRoot())) {

            final PSwing swing = (PSwing) currentNode;
            final PNode grabNode = pickedNode;

            point = new Point(mEvent.getX(), mEvent.getY());
            cameraToLocal(pSwingMouseEvent.getPath().getTopCamera(), point, grabNode);
            prevPoint = (Point) point.clone();

            // This is only partially fixed to find the deepest
            // component at pt. It needs to do something like
            // package private method:
            // Container.getMouseEventTarget(int,int,boolean)
            comp = findShowingComponentAt(swing.getComponent(), point.x, point.y);

            // We found the right component - but we need to
            // get the offset to put the event in the component's
            // coordinates
            if (comp != null && comp != swing.getComponent()) {
                offset = extractSwingOffset(comp, swing);
            }

            // Mouse Pressed gives focus - effects Mouse Drags and
            // Mouse Releases
            if (comp != null && isMousePress(pSwingMouseEvent)) {
                if (SwingUtilities.isLeftMouseButton(mEvent)) {
                    leftButtonData.setState(pickedNode, comp, offset.x, offset.y);
                }
                else if (SwingUtilities.isMiddleMouseButton(mEvent)) {
                    middleButtonData.setState(pickedNode, comp, offset.x, offset.y);
                }
                else if (SwingUtilities.isRightMouseButton(mEvent)) {
                    rightButtonData.setState(pickedNode, comp, offset.x, offset.y);
                }
            }
        }

        // This first case we don't want to give events to just
        // any Swing component - but to the one that got the
        // original mousePressed
        if (isDragOrRelease(pSwingMouseEvent)) {
            if (isLeftMouseButtonOnComponent(mEvent)) {
                handleButton(pSwingMouseEvent, aEvent, leftButtonData);
            }

            if (isMiddleMouseButtonOnComponent(mEvent)) {
                handleButton(pSwingMouseEvent, aEvent, middleButtonData);
            }

            if (isRightMouseButtonOnComponent(mEvent)) {
                handleButton(pSwingMouseEvent, aEvent, rightButtonData);
            }
        }
        else if (isPressOrClickOrMove(pSwingMouseEvent) && comp != null) {
            final MouseEvent tempEvent = new MouseEvent(comp, pSwingMouseEvent.getID(), mEvent.getWhen(), mEvent
                    .getModifiers(), point.x - offset.x, point.y - offset.y, mEvent.getClickCount(), mEvent
                    .isPopupTrigger());

            final PSwingEvent e2 = PSwingMouseEvent.createMouseEvent(tempEvent.getID(), tempEvent, aEvent);
            dispatchEvent(comp, e2);
        }
        else if (isWheelEvent(pSwingMouseEvent) && comp != null) {
            final MouseWheelEvent mWEvent = (MouseWheelEvent) mEvent;

            final MouseWheelEvent tempEvent = new MouseWheelEvent(comp, pSwingMouseEvent.getID(), mEvent.getWhen(),
                    mEvent.getModifiers(), point.x - offset.x, point.y - offset.y, mEvent.getClickCount(), mEvent
                    .isPopupTrigger(), mWEvent.getScrollType(), mWEvent.getScrollAmount(), mWEvent
                    .getWheelRotation());

            final PSwingMouseWheelEvent e2 = new PSwingMouseWheelEvent(tempEvent.getID(), tempEvent, aEvent);
            dispatchEvent(comp, e2);
        }

        // Now we need to check if an exit or enter event needs to
        // be dispatched - this code is independent of the mouseButtons.
        // I tested in normal Swing to see the correct behavior.
        if (previousComponent != null) {
            // This means mouseExited

            // This shouldn't happen - since we're only getting node events
            if (comp == null || pSwingMouseEvent.getID() == MouseEvent.MOUSE_EXITED) {
                final MouseEvent tempEvent = createExitEvent(mEvent);

                final PSwingEvent e2 = PSwingMouseEvent.createMouseEvent(tempEvent.getID(), tempEvent, aEvent);

                dispatchEvent(previousComponent, e2);
                previousComponent = null;
            }

            // This means mouseExited prevComponent and mouseEntered comp
            else if (previousComponent != comp) {
                MouseEvent tempEvent = createExitEvent(mEvent);
                PSwingEvent e2 = PSwingMouseEvent.createMouseEvent(tempEvent.getID(), tempEvent, aEvent);
                dispatchEvent(previousComponent, e2);

                tempEvent = createEnterEvent(comp, mEvent, offset.x, offset.y);
                e2 = PSwingMouseEvent.createMouseEvent(tempEvent.getID(), tempEvent, aEvent);
                comp.dispatchEvent(e2.asMouseEvent());
            }
        }
        else if (comp != null) { // This means mouseEntered
            final MouseEvent tempEvent = createEnterEvent(comp, mEvent, offset.x, offset.y);
            final PSwingEvent e2 = PSwingMouseEvent.createMouseEvent(tempEvent.getID(), tempEvent, aEvent);
            dispatchEvent(comp, e2);
        }

        previousComponent = comp;

        if (comp != null) {
            previousOffset = offset;
        }
    }

    private Point extractSwingOffset(final Component comp, final PSwing swing) {
        int offsetX = 0;
        int offsetY = 0;

        for (Component c = comp; c != swing.getComponent(); c = c.getParent()) {
            offsetX += c.getLocation().x;
            offsetY += c.getLocation().y;
        }

        return new Point(offsetX, offsetY);
    }

    private boolean isRightMouseButtonOnComponent(final MouseEvent mEvent) {
        return SwingUtilities.isRightMouseButton(mEvent) && rightButtonData.getFocusedComponent() != null;
    }

    private boolean isMiddleMouseButtonOnComponent(final MouseEvent mEvent) {
        return SwingUtilities.isMiddleMouseButton(mEvent) && middleButtonData.getFocusedComponent() != null;
    }

    private boolean isLeftMouseButtonOnComponent(final MouseEvent mEvent) {
        return SwingUtilities.isLeftMouseButton(mEvent) && leftButtonData.getFocusedComponent() != null;
    }

    private boolean isMousePress(final PSwingEvent pSwingMouseEvent) {
        return pSwingMouseEvent.getID() == MouseEvent.MOUSE_PRESSED;
    }

    private boolean isWheelEvent(final PSwingEvent pSwingMouseEvent) {
        return pSwingMouseEvent.getID() == MouseEvent.MOUSE_WHEEL;
    }

    private boolean isPressOrClickOrMove(final PSwingEvent pSwingMouseEvent) {
        return isMousePress(pSwingMouseEvent) || pSwingMouseEvent.getID() == MouseEvent.MOUSE_CLICKED
                || pSwingMouseEvent.getID() == MouseEvent.MOUSE_MOVED;
    }

    private boolean isDragOrRelease(final PSwingEvent pSwingMouseEvent) {
        return pSwingMouseEvent.getID() == MouseEvent.MOUSE_DRAGGED
                || pSwingMouseEvent.getID() == MouseEvent.MOUSE_RELEASED;
    }

    private MouseEvent createEnterEvent(final Component comp, final MouseEvent e1, final int offX, final int offY) {
        return new MouseEvent(comp, MouseEvent.MOUSE_ENTERED, e1.getWhen(), 0, (int) prevPoint.getX() - offX,
                (int) prevPoint.getY() - offY, e1.getClickCount(), e1.isPopupTrigger());
    }

    private MouseEvent createExitEvent(final MouseEvent e1) {
        return new MouseEvent(previousComponent, MouseEvent.MOUSE_EXITED, e1.getWhen(), 0, (int) prevPoint.getX()
                - (int) previousOffset.getX(), (int) prevPoint.getY() - (int) previousOffset.getY(),
                e1.getClickCount(), e1.isPopupTrigger());
    }

    private void handleButton(final PSwingEvent e1, final PInputEvent aEvent, final ButtonData buttonData) {
        final MouseEvent m1 = e1.asMouseEvent();
        if (involvesSceneNode(buttonData)) {
            // TODO: this probably won't handle viewing through multiple
            // cameras.

            final Point2D pt = new Point2D.Double(m1.getX(), m1.getY());
            cameraToLocal(e1.getPath().getTopCamera(), pt, buttonData.getPNode());
            final MouseEvent tempEvent = new MouseEvent(buttonData.getFocusedComponent(), m1.getID(), m1.getWhen(), m1
                    .getModifiers(), (int) pt.getX() - buttonData.getOffsetX(), (int) pt.getY()
                    - buttonData.getOffsetY(), m1.getClickCount(), m1.isPopupTrigger());

            final PSwingEvent e2 = PSwingMouseEvent.createMouseEvent(tempEvent.getID(), tempEvent, aEvent);
            dispatchEvent(buttonData.getFocusedComponent(), e2);
        }
        else {
            dispatchEvent(buttonData.getFocusedComponent(), e1);
        }
        // buttonData.getPSwing().repaint(); //Experiment with SliderExample
        // (from Martin) suggests this line is unnecessary, and a serious
        // problem in performance.
        m1.consume();
        if (e1.getID() == MouseEvent.MOUSE_RELEASED) {
            buttonData.mouseReleased();
        }
    }

    private boolean involvesSceneNode(final ButtonData buttonData) {
        return buttonData.getPNode().isDescendentOf(canvas.getRoot());
    }

    private void dispatchEvent(final Component target, final PSwingEvent event) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                target.dispatchEvent(event.asMouseEvent());
            }
        });
    }

    /**
     * Transforms the given point from camera coordinates to the node's local
     * system.
     * 
     * @param camera camera from which coordinates are measured
     * @param pt point to transform (will be modified)
     * @param node node from which local coordinates are measured
     */
    private void cameraToLocal(final PCamera camera, final Point2D pt, final PNode node) {
        if (node != null) {
            if (descendsFromLayer(node)) {
                final AffineTransform inverse = invertTransform(camera.getViewTransform());
                inverse.transform(pt, pt);
            }

            node.globalToLocal(pt);
        }
    }

    /**
     * Returns true if the provided layer has a PLayer ancestor.
     * 
     * @param node node being tested
     * 
     * @return true if node is a descendant of a PLayer
     */
    private boolean descendsFromLayer(final PNode node) {
        PNode searchNode = node;
        while (searchNode != null) {
            searchNode = searchNode.getParent();
            if (searchNode instanceof PLayer) {
                return true;
            }
        }
        return false;
    }

    /**
     * Returns the inverse transform for the provided transform. Throws
     * exception if transform is non invertible.
     * 
     * @param transform transform to invert
     * @return inverted transform
     */
    private AffineTransform invertTransform(final PAffineTransform transform) {
        try {
            return transform.createInverse();
        }
        catch (final NoninvertibleTransformException e) {
            throw new PAffineTransformException(e, transform);
        }
    }

    /**
     * Process a Piccolo2D event and (if active) dispatch the corresponding
     * Swing event.
     * 
     * @param aEvent Piccolo2D event being tested for dispatch to swing
     * @param type is not used in this method
     */
    public void processEvent(final PInputEvent aEvent, final int type) {
        if (!aEvent.isMouseEvent()) {
            return;
        }

        final InputEvent sourceSwingEvent = aEvent.getSourceSwingEvent();
        if (!(sourceSwingEvent instanceof MouseEvent)) {
            throw new RuntimeException("PInputEvent.getSourceSwingEvent was not a MouseEvent.  Actual event: "
                    + sourceSwingEvent + ", class=" + sourceSwingEvent.getClass().getName());
        }

        processMouseEvent(aEvent, (MouseEvent) sourceSwingEvent);
    }

    private void processMouseEvent(final PInputEvent aEvent, final MouseEvent swingMouseEvent) {
        if (!recursing) {
            recursing = true;
            final PSwingEvent pSwingMouseEvent = PSwingMouseEvent.createMouseEvent(swingMouseEvent.getID(),
                    swingMouseEvent, aEvent);
            
            dispatchEvent(pSwingMouseEvent, aEvent);
            if (pSwingMouseEvent.asMouseEvent().isConsumed()) {
                aEvent.setHandled(true);
            }
            recursing = false;
        }
    }

    /**
     * Internal Utility class for handling button interactivity.
     */
    private static class ButtonData {
        private PNode focusNode = null;
        private Component focusComponent = null;
        private int focusOffX = 0;
        private int focusOffY = 0;

        public void setState(final PNode visualNode, final Component comp, final int offX, final int offY) {
            focusComponent = comp;
            focusNode = visualNode;
            focusOffX = offX;
            focusOffY = offY;
        }

        public Component getFocusedComponent() {
            return focusComponent;
        }

        public PNode getPNode() {
            return focusNode;
        }

        public int getOffsetX() {
            return focusOffX;
        }

        public int getOffsetY() {
            return focusOffY;
        }

        public void mouseReleased() {
            focusComponent = null;
            focusNode = null;
        }
    }
}
