/**
 * Copyright (C) 1998-2000 by University of Maryland, College Park, MD 20742, USA
 * All rights reserved.
 */
package edu.umd.cs.piccolox.pswing;

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 javax.swing.*;
import java.awt.*;
import java.awt.event.InputEvent;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;

/**
 * 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 {

    private PNode listenNode = null;  //used to listen to for events
    private boolean active = false; //True when event handlers are set active.

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

    // Previous points used in generating mouseEntered and mouseExited events
    private Point2D prevPoint = null;
    private Point2D prevOff = null;

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

    private ButtonData leftButtonData = new ButtonData();
    private ButtonData rightButtonData = new ButtonData();
    private ButtonData middleButtonData = new ButtonData();

    private PSwingCanvas canvas;

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

    /**
     * Constructs a new PSwingEventHandler for the given canvas.
     */
    public PSwingEventHandler( PSwingCanvas canvas ) {
        this.canvas = canvas;
    }

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

    /**
     * Determines if this event handler is active.
     *
     * @return True if active
     */
    public boolean isActive() {
        return active;
    }

    /**
     * Finds the component at the specified location (must be showing).
     *
     * @param c
     * @param x
     * @param y
     * @return the component at the specified location.
     */
    private Component findShowingComponentAt( Component c, int x, int y ) {
        if( !c.contains( x, y ) ) {
            return null;
        }

        if( c instanceof Container ) {
            Container contain = ( (Container)c );
            int ncomponents = contain.getComponentCount();
            Component component[] = contain.getComponents();

            for( int i = 0; i < ncomponents; i++ ) {
                Component comp = component[i];
                if( comp != null ) {
                    Point p = comp.getLocation();
                    if( comp instanceof Container ) {
                        comp = findShowingComponentAt( comp, x - (int)p.getX(), y - (int)p.getY() );
                    }
                    else {
                        comp = comp.getComponentAt( x - (int)p.getX(), y - (int)p.getY() );
                    }
                    if( comp != null && comp.isShowing() ) {
                        return comp;
                    }
                }
            }
        }
        return c;
    }

    /**
     * Determines if any Swing components in Piccolo
     * 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
     * @param aEvent
     */
    void dispatchEvent( PSwingMouseEvent pSwingMouseEvent, PInputEvent aEvent ) {
        Component comp = null;
        Point2D pt = null;
        PNode pickedNode = pSwingMouseEvent.getPath().getPickedNode();

        // The offsets to put the event in the correct context
        int offX = 0;
        int offY = 0;

        PNode currentNode = pSwingMouseEvent.getCurrentNode();

        if( currentNode instanceof PSwing ) {

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

            if( grabNode.isDescendentOf( canvas.getRoot() ) ) {
                pt = new Point2D.Double( pSwingMouseEvent.getX(), pSwingMouseEvent.getY() );
                cameraToLocal( pSwingMouseEvent.getPath().getTopCamera(), pt, grabNode );
                prevPoint = new Point2D.Double( pt.getX(), pt.getY() );

                // 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(), (int)pt.getX(), (int)pt.getY() );

                // 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() ) {
                    for( Component c = comp; c != swing.getComponent(); c = c.getParent() ) {
                        offX += c.getLocation().getX();
                        offY += c.getLocation().getY();
                    }
                }

                // Mouse Pressed gives focus - effects Mouse Drags and
                // Mouse Releases
                if( comp != null && pSwingMouseEvent.getID() == MouseEvent.MOUSE_PRESSED ) {
                    if( SwingUtilities.isLeftMouseButton( pSwingMouseEvent ) ) {
                        leftButtonData.setState( swing, pickedNode, comp, offX, offY );
                    }
                    else if( SwingUtilities.isMiddleMouseButton( pSwingMouseEvent ) ) {
                        middleButtonData.setState( swing, pickedNode, comp, offX, offY );
                    }
                    else if( SwingUtilities.isRightMouseButton( pSwingMouseEvent ) ) {
                        rightButtonData.setState( swing, pickedNode, comp, offX, offY );
                    }
                }
            }
        }

        // 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( pSwingMouseEvent.getID() == MouseEvent.MOUSE_DRAGGED ||
            pSwingMouseEvent.getID() == MouseEvent.MOUSE_RELEASED ) {

            // LEFT MOUSE BUTTON
            if( SwingUtilities.isLeftMouseButton( pSwingMouseEvent ) && leftButtonData.getFocusedComponent() != null ) {
                handleButton( pSwingMouseEvent, aEvent, leftButtonData );
            }

            // MIDDLE MOUSE BUTTON
            if( SwingUtilities.isMiddleMouseButton( pSwingMouseEvent ) && middleButtonData.getFocusedComponent() != null )
            {
                handleButton( pSwingMouseEvent, aEvent, middleButtonData );
            }

            // RIGHT MOUSE BUTTON
            if( SwingUtilities.isRightMouseButton( pSwingMouseEvent ) && rightButtonData.getFocusedComponent() != null )
            {
                handleButton( pSwingMouseEvent, aEvent, rightButtonData );
            }
        }
        // This case covers the cases mousePressed, mouseClicked,
        // and mouseMoved events
        else if( ( pSwingMouseEvent.getID() == MouseEvent.MOUSE_PRESSED ||
                   pSwingMouseEvent.getID() == MouseEvent.MOUSE_CLICKED ||
                   pSwingMouseEvent.getID() == MouseEvent.MOUSE_MOVED ) &&
                                                                        ( comp != null ) ) {

            MouseEvent e_temp = new MouseEvent( comp,
                                                pSwingMouseEvent.getID(),
                                                pSwingMouseEvent.getWhen(),
                                                pSwingMouseEvent.getModifiers(),
                                                (int)pt.getX() - offX,
                                                (int)pt.getY() - offY,
                                                pSwingMouseEvent.getClickCount(),
                                                pSwingMouseEvent.isPopupTrigger() );

            PSwingMouseEvent e2 = PSwingMouseEvent.createMouseEvent( e_temp.getID(), e_temp, aEvent );
            dispatchEvent( comp, e2 );
            pSwingMouseEvent.consume();
        }

        // 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( prevComponent != null ) {
            // This means mouseExited

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

                PSwingMouseEvent e2 = PSwingMouseEvent.createMouseEvent( e_temp.getID(), e_temp, aEvent );

                dispatchEvent( prevComponent, e2 );
                prevComponent = null;

                if( pSwingMouseEvent.getID() == MouseEvent.MOUSE_EXITED ) {
                    pSwingMouseEvent.consume();
                }
            }

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

                e_temp = createEnterEvent( comp, pSwingMouseEvent, offX, offY );
                e2 = PSwingMouseEvent.createMouseEvent( e_temp.getID(), e_temp, aEvent );
                comp.dispatchEvent( e2 );
            }
        }
        else {
            // This means mouseEntered
            if( comp != null ) {
                MouseEvent e_temp = createEnterEvent( comp, pSwingMouseEvent, offX, offY );
                PSwingMouseEvent e2 = PSwingMouseEvent.createMouseEvent( e_temp.getID(), e_temp, aEvent );
                dispatchEvent( comp, e2 );
            }
        }

        //todo add cursors
//        // We have to manager our own Cursors since this is normally
//        // done on the native side
//        if( comp != cursorComponent &&
//            focusNodeLeft == null &&
//            focusNodeMiddle == null &&
//            focusNodeRight == null ) {
//            if( comp != null ) {
//                cursorComponent = comp;
//                canvas.setCursor( comp.getCursor(), false );
//            }
//            else {
//                cursorComponent = null;
//                canvas.resetCursor();
//            }
//        }

        // Set the previous variables for next time
        prevComponent = comp;

        if( comp != null ) {
            prevOff = new Point2D.Double( offX, offY );
        }
    }

    private MouseEvent createEnterEvent( Component comp, PSwingMouseEvent e1, int offX, 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( PSwingMouseEvent e1 ) {
        return new MouseEvent( prevComponent, MouseEvent.MOUSE_EXITED,
                               e1.getWhen(), 0,
                               (int)prevPoint.getX() - (int)prevOff.getX(), (int)prevPoint.getY() - (int)prevOff.getY(),
                               e1.getClickCount(), e1.isPopupTrigger() );
    }

    private void handleButton( PSwingMouseEvent e1, PInputEvent aEvent, ButtonData buttonData ) {
        Point2D pt;
        if( buttonData.getPNode().isDescendentOf( canvas.getRoot() ) ) {
            pt = new Point2D.Double( e1.getX(), e1.getY() );
            cameraToLocal( e1.getPath().getTopCamera(), pt, buttonData.getPNode() );
            //todo this probably won't handle viewing through multiple cameras.
            MouseEvent e_temp = new MouseEvent( buttonData.getFocusedComponent(),
                                                e1.getID(), e1.getWhen(), e1.getModifiers(),
                                                (int)pt.getX() - buttonData.getOffsetX(),
                                                (int)pt.getY() - buttonData.getOffsetY(),
                                                e1.getClickCount(), e1.isPopupTrigger() );

            PSwingMouseEvent e2 = PSwingMouseEvent.createMouseEvent( e_temp.getID(), e_temp, 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.
        e1.consume();
        if( e1.getID() == MouseEvent.MOUSE_RELEASED ) {
            buttonData.mouseReleased();
        }
    }

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

    private void cameraToLocal( PCamera topCamera, Point2D pt, PNode node ) {
        AffineTransform inverse = null;
        try {
            inverse = topCamera.getViewTransform().createInverse();
        }
        catch( NoninvertibleTransformException e ) {
            e.printStackTrace();
        }
        
        /* Only apply the camera's view transform when this node is a descendant of PLayer */
        PNode searchNode = node;
        do {
             searchNode = searchNode.getParent();
             if (searchNode instanceof PLayer) {
                 inverse.transform( pt, pt );
                 break;
             }
        } while(searchNode != null);
        
        if( node != null ) {
            node.globalToLocal( pt );
        }
        return;
    }

    /**
     * Process a piccolo event and (if active) dispatch the corresponding Swing event.
     *
     * @param aEvent
     * @param type
     */
    public void processEvent( PInputEvent aEvent, int type ) {
        if(aEvent.isMouseEvent()) {
            InputEvent sourceSwingEvent = aEvent.getSourceSwingEvent();
            if (sourceSwingEvent instanceof MouseEvent) {
            	MouseEvent swingMouseEvent = (MouseEvent) sourceSwingEvent;
                PSwingMouseEvent pSwingMouseEvent = PSwingMouseEvent.createMouseEvent( swingMouseEvent.getID(), swingMouseEvent, aEvent );
                if( !recursing ) {
                    recursing = true;
                    dispatchEvent( pSwingMouseEvent, aEvent );
                    recursing = false;
                }
            } else {
                new Exception("PInputEvent.getSourceSwingEvent was not a MouseEvent.  Actual event: " + sourceSwingEvent + ", class=" + sourceSwingEvent.getClass().getName() ).printStackTrace();
            }
        }
    	
/*        if( !( EventQueue.getCurrentEvent() instanceof MouseEvent ) ) {
            new Exception( "EventQueue.getCurrentEvent was not a MouseEvent, consider making PInputEvent.getSourceSwingEvent public.  Actual event: " + EventQueue.getCurrentEvent() + ", class=" + EventQueue.getCurrentEvent().getClass().getName() ).printStackTrace();
        }
        if( aEvent.isMouseEvent() && EventQueue.getCurrentEvent() instanceof MouseEvent ) {
            MouseEvent sourceSwingEvent = (MouseEvent)EventQueue.getCurrentEvent();
            PSwingMouseEvent pSwingMouseEvent = PSwingMouseEvent.createMouseEvent( sourceSwingEvent.getID(), sourceSwingEvent, aEvent );
            if( !recursing ) {
                recursing = true;
                dispatchEvent( pSwingMouseEvent, aEvent );
                recursing = false;
            }
        }
*/
    }

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

        public void setState( PSwing swing, PNode visualNode, Component comp, int offX, int offY ) {
            focusPSwing = swing;
            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 PSwing getPSwing() {
            return focusPSwing;
        }

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