/*
 * @(#)VirtualWindow.java
 *
 * Copyright (C) 2002-2003 Matt Albrecht
 * groboclown@users.sourceforge.net
 * http://groboutils.sourceforge.net
 *
 *  Permission is hereby granted, free of charge, to any person obtaining a
 *  copy of this software and associated documentation files (the "Software"),
 *  to deal in the Software without restriction, including without limitation
 *  the rights to use, copy, modify, merge, publish, distribute, sublicense,
 *  and/or sell copies of the Software, and to permit persons to whom the 
 *  Software is furnished to do so, subject to the following conditions:
 *
 *  The above copyright notice and this permission notice shall be included in 
 *  all copies or substantial portions of the Software. 
 *
 *  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL 
 *  THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
 *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
 *  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 
 *  DEALINGS IN THE SOFTWARE.
 */

package net.sourceforge.groboutils.uicapture.v1;

import java.awt.Robot;
import java.awt.Frame;
import java.awt.Rectangle;
import java.awt.image.BufferedImage;
import java.awt.event.KeyListener;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelListener;
import java.awt.event.MouseEvent;
import java.awt.event.KeyEvent;
import java.awt.event.MouseWheelEvent;

import java.util.LinkedList;
import java.util.Iterator;

import net.sourceforge.groboutils.uicapture.v1.event.ICaptureListener;
import net.sourceforge.groboutils.uicapture.v1.event.CaptureEvent;
import net.sourceforge.groboutils.uicapture.v1.event.KeyPressedCaptureEvent;
import net.sourceforge.groboutils.uicapture.v1.event.KeyReleasedCaptureEvent;
import net.sourceforge.groboutils.uicapture.v1.event.MousePressedCaptureEvent;
import net.sourceforge.groboutils.uicapture.v1.event.MouseReleasedCaptureEvent;
import net.sourceforge.groboutils.uicapture.v1.event.MouseMovedCaptureEvent;
import net.sourceforge.groboutils.uicapture.v1.event.MouseWheelCaptureEvent;
import net.sourceforge.groboutils.uicapture.v1.event.IAllowCapturePassThroughListener;

/**
 * A window which covers the whole screen, and does not paint in the background.
 * It captures keyboard and mouse events, and sends them to both all registered
 * listeners, and to the underlying GUI as well.  This transparent window is
 * similar to the "glass pane" concept in Swing JFrames.
 * <P>
 * For the moment, there is no way for listeners to prevent an event from
 * being passed to the underlying UI.  This needs to be changed.
 * <P>
 * WARNING: if the screen size is to resize, then this will not work correctly.
 *
 * @author    Matt Albrecht <a href="mailto:groboclown@users.sourceforge.net">groboclown@users.sourceforge.net</a>
 * @since     Jan 4, 2002
 * @version   Mar 13, 2002
 */
public class VirtualWindow
        implements KeyListener, MouseListener, MouseMotionListener,
            MouseWheelListener
{
    //-------------------------------------------------------------------------
    // Private fields
    
    private LinkedList captureListeners = new LinkedList();
    private Robot robot = null;
    private VirtualWindowUI window = null;
    private boolean enableGlass = true;
    private Frame owningFrame = null;
    
    private static final String DEFAULT_TITLE = "UI Capture";
    
    //-------------------------------------------------------------------------
    // Constructors

    
    /**
     * Create a new VirtualWindow, with the glass enabled.
     *
     * @exception java.awt.AWTException thrown if a Robot is not supported
     *      in the current JDK implementation.
     */
    public VirtualWindow()
            throws java.awt.AWTException
    {
        this( null, true );
    }
    
    
    /**
     * Specify the initial enabled state of the window.
     *
     * @param enable set to the initial glass pane state.
     * @exception java.awt.AWTException thrown if a Robot is not supported
     *      in the current JDK implementation.
     */
    public VirtualWindow( String title, boolean enable )
            throws java.awt.AWTException
    {
        if (title == null)
        {
            title = DEFAULT_TITLE;
        }
        this.owningFrame = new Frame( title );
        this.owningFrame.setSize( 0, 0 );
        // this.owningFrame.show();
        // this.owningFrame.hide();
        this.window = new VirtualWindowUI( this.owningFrame );
        this.robot = new Robot();
        
        this.window.addKeyListener( this );
        this.window.addMouseListener( this );
        this.window.addMouseMotionListener( this );
        this.window.addMouseWheelListener( this );
        
        update();

        setGlassEnabled( enable );
    }
    


    //-------------------------------------------------------------------------
    // Public methods

    
    /**
     * Close out all inner instances and shut down the UI
     */
    public void dispose()
    {
        this.window.removeKeyListener( this );
        this.window.removeMouseListener( this );
        this.window.removeMouseMotionListener( this );
        this.window.removeMouseWheelListener( this );
        
        this.window.dispose();
        this.owningFrame.dispose();
        this.captureListeners.clear();
        this.robot = null;
        this.window = null;
    }
    
    
    /**
     * 
     * @return the inner Window reference.
     */
    public VirtualWindowUI getWindow()
    {
        return this.window;
    }
    
    
    /**
     * Sets the inner state for displaying the glass pane.  If the pane is
     * enabled, then the glass pane will attempt to maximize itself and
     * keep itself on the foreground at all costs.
     *
     * @param on <tt>true</tt> if the glass pane is enabled (active and
     *      intercepting events), or <tt>false</tt> if is disabled.
     */
    public synchronized void setGlassEnabled( boolean on )
    {
        this.enableGlass = on;
        this.window.setGlassEnabled( on );
    }
    
    
    /**
     * Retrieves the current glass enabled state.
     *
     * @return <tt>true</tt> if the glass pane is enabled (active and
     *      intercepting events), or <tt>false</tt> if is disabled.
     */
    public boolean isGlassEnabled()
    {
        return this.enableGlass;
    }
    
    
    /**
     * Simulates the given captured event.  This minimizes the glass window,
     * performs the event with the Robot, and restores the glass window if
     * the glass pane is enabled.
     *
     * @param ce the event to simulate.
     */
    public synchronized void simulateEvent( CaptureEvent ce )
    {
        hide();

        // ensure that the window is restored, no matter the exception.
        try
        {
            ce.performEvent( this.robot );
        }
        finally
        {
            show();
        }
    }
    
    
    /**
     * Sleeps for the specified number of milliseconds.  This is passed
     * directly through to the underlying Robot.
     *
     * @param ms Time to sleep in milliseconds.
     * @exception IllegalArgumentException thrown by Robot if <tt>ms</tt> is
     *		not between 0 and 60,000, inclusive.
     */
    public void delay( int ms )
    {
        this.robot.delay( ms );
    }
    
    
    /**
     * Waits until all events currently on the event queue have been processed.
     * This is passed directly through to the underlying Robot.
     */
    public void waitForIdle()
    {
        this.robot.waitForIdle();
    }
    
    
    /**
     * Scrapes the current screen into a BufferedImage the same size as the
     * window.
     *
     * @return the captured screen image.
     */
    public BufferedImage createScreenScrape()
    {
        return createScreenScrape( this.window.getCoveredScreen() );
    }
    
    
    /**
     * Scrapes the current screen into a BufferedImage from the given area
     * on the screen.  This is passed directly to the underlying Robot.
     *
     * @return the captured screen image.
     */
    public BufferedImage createScreenScrape( Rectangle bounds )
    {
        // check bounds against the window's bounds
        if (bounds == null ||
            !this.window.getCoveredScreen().contains( bounds ))
        {
            throw new IllegalArgumentException("Bounds "+bounds+
                " is not within the screen dimension.");
        }
        
        // ensure the window is hidden, to get the current screen image,
        // then restore the window, no matter the thrown exceptions.
        hide();
        
        try
        {
            return this.robot.createScreenCapture( bounds );
        }
        finally
        {
            show();
        }
    }
    
    
    /**
     * Adds an <tt>ICaptureListener</tt> to the list of recipients of input
     * events.  If the given listener is <tt>null</tt>, then the request is
     * ignored.
     *
     * @param cl the listener to add.
     */
    public void addCaptureListener( ICaptureListener cl )
    {
        if (cl != null)
        {
            this.captureListeners.add( cl );
        }
    }
    
    
    /**
     * Removes the given <tt>ICaptureListener</tt> from the inner list of
     * input events recipients.  If the given listener is <tt>null</tt> or
     * not registered, then the request is ignored.
     *
     * @param cl the listener to remove.
     */
    public void removeCaptureListener( ICaptureListener cl )
    {
        if (cl != null)
        {
            this.captureListeners.remove( cl );
        }
    }
    
    
    /**
     * Hides the glass pane, and stops all input event capturing.  This is
     * only executed if the glass is enabled, and has no effect on the
     * enabled state of the glass.
     */
    public void hide()
    {
        if (this.enableGlass)
        {
            this.window.hide();
        }
    }
    
    
    /**
     * Shows the glass pane, and continues all input event capturing.  This is
     * only executed if the glass is enabled, and has no effect on the
     * enabled state of the glass.
     */
    public void show()
    {
        if (this.enableGlass)
        {
            this.window.show();
        }
    }
    
    
    /**
     * Updates the background image.
     */
    public void update()
    {
        this.window.setBackground( createScreenScrape() );
    }
    
    
    //-------------------------------------------------------------------------
    // Event methods
    
    
    
    /**
     * @see java.awt.event.MouseWheelListener
     */
    public void mouseWheelMoved( MouseWheelEvent me )
    {
        MouseWheelCaptureEvent ce = new MouseWheelCaptureEvent( me );
        Iterator iter = getCaptureListeners();
        boolean allow = true;
        while (iter.hasNext())
        {
            ICaptureListener icl = (ICaptureListener)iter.next();
            if (icl instanceof IAllowCapturePassThroughListener)
            {
                if (!((IAllowCapturePassThroughListener)icl).
                    allowMouseWheelMoved( ce ))
                {
                    allow = false;
                }
            }
            icl.mouseWheelMoved( ce );
        }
        if (allow)
        {
            simulateEvent( ce );
        }
    }
    
    
    /**
     * @see java.awt.event.MouseMotionListener
     */
    public void mouseDragged( MouseEvent me )
    {
        // ignore this method
    }
    
    
    /**
     * @see java.awt.event.MouseMotionListener
     */
    public void mouseMoved( MouseEvent me )
    {
        MouseMovedCaptureEvent ce = new MouseMovedCaptureEvent( me );
        
        // do not need to simulate event: the mouse already moved.
        // simulateEvent( ce );
        
        Iterator iter = getCaptureListeners();
        while (iter.hasNext())
        {
            ((ICaptureListener)iter.next()).mouseMoved( ce );
        }
    }
    
    
    /**
     * @see java.awt.event.MouseListener
     */
    public void mousePressed( MouseEvent me )
    {
        MousePressedCaptureEvent ce = new MousePressedCaptureEvent( me );
        Iterator iter = getCaptureListeners();
        boolean allow = true;
        while (iter.hasNext())
        {
            ICaptureListener icl = (ICaptureListener)iter.next();
            if (icl instanceof IAllowCapturePassThroughListener)
            {
                if (!((IAllowCapturePassThroughListener)icl).
                    allowMousePressed( ce ))
                {
                    allow = false;
                }
            }
            icl.mousePressed( ce );
        }
        if (allow)
        {
            simulateEvent( ce );
        }
    }
    
    
    /**
     * @see java.awt.event.MouseListener
     */
    public void mouseReleased( MouseEvent me )
    {
        MouseReleasedCaptureEvent ce = new MouseReleasedCaptureEvent( me );
        Iterator iter = getCaptureListeners();
        boolean allow = true;
        while (iter.hasNext())
        {
            ICaptureListener icl = (ICaptureListener)iter.next();
            if (icl instanceof IAllowCapturePassThroughListener)
            {
                if (!((IAllowCapturePassThroughListener)icl).
                    allowMouseReleased( ce ))
                {
                    allow = false;
                }
            }
            icl.mouseReleased( ce );
        }
        if (allow)
        {
            simulateEvent( ce );
        }
    }
    
    
    
    /**
     * @see java.awt.event.MouseListener
     */
    public void mouseClicked( MouseEvent me )
    {
        // ignore this method
    }
    
    
    
    /**
     * @see java.awt.event.MouseListener
     */
    public void mouseEntered( MouseEvent me )
    {
        // ignore this method
    }
    
    
    
    /**
     * @see java.awt.event.MouseListener
     */
    public void mouseExited( MouseEvent me )
    {
        // ignore this method
    }
    
    
    
    /**
     * @see java.awt.event.KeyListener
     */
    public void keyTyped( KeyEvent me )
    {
        // ignore this method
    }
    
    
    /**
     * @see java.awt.event.KeyListener
     */
    public void keyPressed( KeyEvent ke )
    {
        KeyPressedCaptureEvent ce = new KeyPressedCaptureEvent( ke );
        Iterator iter = getCaptureListeners();
        boolean allow = true;
        while (iter.hasNext())
        {
            ICaptureListener icl = (ICaptureListener)iter.next();
            if (icl instanceof IAllowCapturePassThroughListener)
            {
                if (!((IAllowCapturePassThroughListener)icl).
                    allowKeyPressed( ce ))
                {
                    allow = false;
                }
            }
            icl.keyPressed( ce );
        }
        if (allow)
        {
            simulateEvent( ce );
        }
    }
    
    
    /**
     * @see java.awt.event.KeyListener
     */
    public void keyReleased( KeyEvent ke )
    {
        KeyReleasedCaptureEvent ce = new KeyReleasedCaptureEvent( ke );
        Iterator iter = getCaptureListeners();
        boolean allow = true;
        while (iter.hasNext())
        {
            ICaptureListener icl = (ICaptureListener)iter.next();
            if (icl instanceof IAllowCapturePassThroughListener)
            {
                if (!((IAllowCapturePassThroughListener)icl).
                    allowKeyReleased( ce ))
                {
                    allow = false;
                }
            }
            icl.keyReleased( ce );
        }
        if (allow)
        {
            simulateEvent( ce );
        }
    }
    
    
    
    //-------------------------------------------------------------------------
    // Protected methods
    
    
    /**
     * Returns a list of all the current ICaptureListeners.
     *
     * @return an iterator of the listeners.
     */
    protected Iterator getCaptureListeners()
    {
        return this.captureListeners.iterator();
    }
}

