/*
 * Copyright (c) 2009-2010 jMonkeyEngine
 * 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.
 *
 * * Neither the name of 'jMonkeyEngine' nor 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 com.jme3.input.awt;

import java.awt.Component;
import java.awt.Cursor;
import java.awt.Point;
import java.awt.Robot;
import java.awt.Toolkit;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.awt.image.BufferedImage;
import java.util.ArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.swing.SwingUtilities;

import com.jme3.input.MouseInput;
import com.jme3.input.RawInputListener;
import com.jme3.input.event.MouseButtonEvent;
import com.jme3.input.event.MouseMotionEvent;

/**
 * <code>AwtMouseInput</code>
 *
 * @author Joshua Slack
 * @author MHenze (cylab)
 * 
 * @version $Revision$
 */
public class AwtMouseInput implements MouseInput, MouseListener, MouseWheelListener, MouseMotionListener {

    //public static int WHEEL_AMP = 40;   // arbitrary...  Java's mouse wheel seems to report something a lot lower than lwjgl's

    private static final Logger logger = Logger.getLogger(AwtMouseInput.class.getName());

    private boolean visible = true;

    private RawInputListener listener;

    private Component component;

    private final ArrayList<MouseButtonEvent> eventQueue = new ArrayList<MouseButtonEvent>();
    private final ArrayList<MouseButtonEvent> eventQueueCopy = new ArrayList<MouseButtonEvent>();
    
    private int lastEventX;
    private int lastEventY;
    private int lastEventWheel;

    private Cursor transparentCursor;

    private Robot robot;
    private int wheelPos;
    private Point location;
    private Point centerLocation;
    private Point centerLocationOnScreen;
    private Point lastKnownLocation;
    private boolean isRecentering;
    private boolean cursorMoved;
    private int eventsSinceRecenter;

    public AwtMouseInput() {
        location = new Point();
        centerLocation = new Point();
        centerLocationOnScreen = new Point();
        lastKnownLocation = new Point();

        try{
            robot = new Robot();
        }catch (java.awt.AWTException e){
            logger.log(Level.SEVERE, "Could not create a robot, so the mouse cannot be grabbed! ", e);
        }
    }
    
    public void setInputSource(Component comp){
        if (component != null){
            component.removeMouseListener(this);
            component.removeMouseMotionListener(this);
            component.removeMouseWheelListener(this);
            
            eventQueue.clear();
            
            wheelPos = 0;
            isRecentering = false;
            eventsSinceRecenter = 0;
            lastEventX = 0;
            lastEventY = 0;
            lastEventWheel = 0;
            location = new Point();
            centerLocation = new Point();
            centerLocationOnScreen = new Point();
            lastKnownLocation = new Point();
        }

        component = comp;
        component.addMouseListener(this);
        component.addMouseMotionListener(this);
        component.addMouseWheelListener(this);
    }

    public void initialize() {
    }

    public void destroy() {
    }

    public boolean isInitialized() {
        return true;
    }

    public void setInputListener(RawInputListener listener){
        this.listener = listener;
    }

    public long getInputTimeNanos() {
        return System.nanoTime();
    }

    public void setCursorVisible(boolean visible){
        if (this.visible != visible){
            
            lastKnownLocation.x = lastKnownLocation.y = 0;
            
            this.visible = visible;
            final boolean newVisible = visible;
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    component.setCursor(newVisible ? null : getTransparentCursor());
                    if (!newVisible)
                        recenterMouse(component);
                }
            });
        }
    }

    public void update() {
        if (cursorMoved){
            int newX = location.x;
            int newY = location.y;
            int newWheel = wheelPos;

            // invert DY
            int actualX = lastKnownLocation.x;
            int actualY = component.getHeight() - lastKnownLocation.y;
            MouseMotionEvent evt = new MouseMotionEvent(actualX, actualY,
                                                        newX - lastEventX,
                                                        lastEventY - newY,
                                                        wheelPos, lastEventWheel - wheelPos);
            listener.onMouseMotionEvent(evt);

            lastEventX = newX;
            lastEventY = newY;
            lastEventWheel = newWheel;
            
            cursorMoved = false;
        }

        synchronized (eventQueue){
            eventQueueCopy.clear();
            eventQueueCopy.addAll(eventQueue);
            eventQueue.clear();
        }
        
        int size = eventQueueCopy.size();
        for (int i = 0; i < size; i++){
            listener.onMouseButtonEvent(eventQueueCopy.get(i));
        }
    }

    private Cursor getTransparentCursor() {
        if (transparentCursor == null){
            BufferedImage cursorImage = new BufferedImage(1, 1, BufferedImage.TYPE_INT_ARGB);
            cursorImage.setRGB(0, 0, 0);
            transparentCursor = Toolkit.getDefaultToolkit().createCustomCursor(cursorImage, new Point(0, 0), "empty cursor");
        }
        return transparentCursor;
    }

//	public void setHardwareCursor(URL file, int xHotspot, int yHotspot) {
//	    //Create the image from the provided url
//	    java.awt.Image cursorImage = new ImageIcon( file ).getImage( );
//	    //Create a custom cursor with this image
//	    opaqueCursor = Toolkit.getDefaultToolkit().createCustomCursor( cursorImage , new Point( xHotspot , yHotspot ) , "custom cursor" );
//	    //Use this cursor
//	    setCursorVisible( isCursorVisible );
//	}


    public int getButtonCount() {
        return 3;
    }

    public void mouseClicked(MouseEvent arg0) {
//        MouseButtonEvent evt = new MouseButtonEvent(getJMEButtonIndex(arg0), false);
//        listener.onMouseButtonEvent(evt);
    }

    public void mousePressed(MouseEvent arg0) {
        MouseButtonEvent evt = new MouseButtonEvent(getJMEButtonIndex(arg0), true, arg0.getX(), arg0.getY());
        evt.setTime(arg0.getWhen());
        synchronized (eventQueue){
            eventQueue.add(evt);
        }
    }

    public void mouseReleased(MouseEvent arg0) {
        MouseButtonEvent evt = new MouseButtonEvent(getJMEButtonIndex(arg0), false, arg0.getX(), arg0.getY());
        evt.setTime(arg0.getWhen());
        synchronized (eventQueue){
            eventQueue.add(evt);
        }
    }

    public void mouseEntered(MouseEvent arg0) {
        if (!visible)
            recenterMouse(arg0.getComponent());
    }

    public void mouseExited(MouseEvent arg0) {
        if (!visible)
            recenterMouse(arg0.getComponent());
    }

    public void mouseWheelMoved(MouseWheelEvent arg0) {
        int dwheel = arg0.getUnitsToScroll();
        wheelPos -= dwheel;
        cursorMoved = true;
    }

    public void mouseDragged(MouseEvent arg0) {
        mouseMoved(arg0);
    }

    public void mouseMoved(MouseEvent arg0) {
        if (isRecentering) {
            // MHenze (cylab) Fix Issue 35:
            // As long as the MouseInput is in recentering mode, nothing is done until the mouse is entered in the component
            // by the events generated by the robot. If this happens, the last known location is resetted.
            if ((centerLocation.x == arg0.getX() && centerLocation.y == arg0.getY()) || eventsSinceRecenter++ == 5) {
                lastKnownLocation.x = arg0.getX();
                lastKnownLocation.y = arg0.getY();
                isRecentering = false;
            }
        } else {
            // MHenze (cylab) Fix Issue 35:
            // Compute the delta and absolute coordinates and recenter the mouse if necessary
            int dx = arg0.getX() - lastKnownLocation.x;
            int dy = arg0.getY() - lastKnownLocation.y;
            location.x += dx;
            location.y += dy;
            if (!visible) {
                recenterMouse(arg0.getComponent());
            }
            lastKnownLocation.x = arg0.getX();
            lastKnownLocation.y = arg0.getY();
            
            cursorMoved = true;
        }
    }

    // MHenze (cylab) Fix Issue 35: A method to generate recenter the mouse to allow the InputSystem to "grab" the mouse
    private void recenterMouse(final Component component) {
        if (robot != null) {
            eventsSinceRecenter = 0;
            isRecentering = true;
            centerLocation.setLocation(component.getWidth()/2, component.getHeight()/2);
            centerLocationOnScreen.setLocation(centerLocation);
            SwingUtilities.convertPointToScreen(centerLocationOnScreen, component);
            robot.mouseMove(centerLocationOnScreen.x, centerLocationOnScreen.y);
        }
    }

    private int getJMEButtonIndex( MouseEvent arg0 ) {
        int index;
        switch (arg0.getButton()) {
            default:
            case MouseEvent.BUTTON1: //left
                index = MouseInput.BUTTON_LEFT;
                break;
            case MouseEvent.BUTTON2: //middle
                index = MouseInput.BUTTON_MIDDLE;
                break;
            case MouseEvent.BUTTON3: //right
                index = MouseInput.BUTTON_RIGHT;
                break;
        }
        return index;
    }
}
