/*
 * Copyright (c) 2000-2009 TeamDev Ltd. All rights reserved.
 * TeamDev PROPRIETARY and CONFIDENTIAL.
 * Use is subject to license terms.
 */
package com.jniwrapper.win32.hook;

import com.jniwrapper.*;
import com.jniwrapper.util.EnumItem;
import com.jniwrapper.util.Logger;
import com.jniwrapper.win32.hook.data.HooksData;
import com.jniwrapper.win32.system.EventObject;

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

/**
 * This class enables to install various Windows hooks and retrieve information from them
 * using {@link HookEventListener}.
 * <p/>
 * All avaiable hooks are represented by the {@link Descriptor} class.
 *
 * @author Serge Piletsky
 */
public class Hook
{
    private static final Logger LOG = Logger.getInstance(Hook.class);

    /**
     * This class represents the enumeraton of available Windows hooks.
     *
     * @author Serge Piletsky
     */
    public static class Descriptor extends EnumItem
    {
        /**
         * Records input messages posted to the system message queue.
         */
        public static final Descriptor JOURNALRECORD = new Descriptor(0, "JOURNALRECORD", true);

        /**
         * Posts messages previously recorded by a {@link #JOURNALRECORD} hook procedure.
         */
        public static final Descriptor JOURNALPLAYBACK = new Descriptor(1, "JOURNALPLAYBACK", true);

        /**
         * Monitors keystroke messages.
         */
        public static final Descriptor KEYBOARD = new Descriptor(2, "KEYBOARD");

        /**
         * Monitors messages posted to a message queue.
         */
        public static final Descriptor GETMESSAGE = new Descriptor(3, "GETMESSAGE");

        /**
         * Monitors messages before the system sends them to the destination window procedure.
         */
        public static final Descriptor CALLWNDPROC = new Descriptor(4, "CALLWNDPROC");

        /**
         * Receives notifications useful to a computer-based training (CBT) application.
         */
        public static final Descriptor CBT = new Descriptor(5, "CBT");

        /**
         * Monitors messages generated as a result of an input event in a dialog box, message box, menu, or scroll bar.
         */
        public static final Descriptor SYSMSGFILTER = new Descriptor(6, "SYSMSGFILTER", true);

        /**
         * Monitors mouse messages.
         */
        public static final Descriptor MOUSE = new Descriptor(7, "MOUSE");

        /**
         *
         */
        public static final Descriptor HARDWARE = new Descriptor(8, "HARDWARE");

        /**
         * Useful for debugging other hook procedures.
         */
        public static final Descriptor DEBUG = new Descriptor(9, "DEBUG");

        /**
         * Receives notifications useful to shell applications.
         */
        public static final Descriptor SHELL = new Descriptor(10, "SHELL");

        /**
         * A hook procedure that will be called when the application's foreground thread is about to become idle.
         * This hook is useful for performing low priority tasks during idle time.
         */
        public static final Descriptor FOREGROUNDIDLE = new Descriptor(11, "FOREGROUNDIDLE");

        /**
         * Monitors messages after they have been processed by the destination window procedure.
         */
        public static final Descriptor CALLWNDPROCRET = new Descriptor(12, "CALLWNDPROCRET");

        /**
         * Monitors low-level keyboard input events.
         * Available for WinNT systems only.
         */
        public static final Descriptor KEYBOARD_LL = new Descriptor(13, "KEYBOARD_LL", true);

        /**
         * Monitors low-level mouse input events.
         */
        public static final Descriptor MOUSE_LL = new Descriptor(14, "MOUSE_LL", true);

        private static final String NAME_PREFIX = "JNIWrapperHook.";

        private boolean _globalOnly;
        private String _name = null;

        private Descriptor(int value, String name)
        {
            this(value, name, false);
        }

        private Descriptor(int value, String name, boolean globalOnly)
        {
            super(value);
            _globalOnly = globalOnly;
            _name = NAME_PREFIX + name;
        }

        /**
         * Returns true if the HookDescriptor, which is determinated by this item, is global only.
         *
         * @return true if the HookDescriptor is global only.
         */
        public boolean isGlobalOnly()
        {
            return _globalOnly;
        }

        /**
         * Returns the string descriptor.
         *
         * @return the string descriptor.
         */
        public String getName()
        {
            return _name;
        }
    }

    private static final String FUNCTION_InstallHook = "InstallHook";
    private static final String FUNCTION_UninstallHook = "UninstallHook";

    private static Library _library;

    protected final List _listeners = new LinkedList();

    private Thread _eventsThread = null;
    private HookEventLoop _eventLoop = null;
    private Descriptor _descriptor;
    private boolean _installed = false;
    private final HooksData _hooksData = new HooksData();
    private boolean _synchronous = false;

    /**
     * Hook described by {@link Descriptor#JOURNALRECORD} descriptor.
     */
    public static final Hook JOURNALRECORD = new Hook(Descriptor.JOURNALRECORD);

    /**
     * Hook described by {@link Descriptor#KEYBOARD} descriptor.
     */
    public static final Hook KEYBOARD = new Hook(Descriptor.KEYBOARD);

    /**
     * Hook described by {@link Descriptor#KEYBOARD_LL} descriptor.
     */
    public static final Hook KEYBOARD_LL = new LowLevelKeyboardHook();

    /**
     * Hook described by {@link Descriptor#GETMESSAGE} descriptor.
     */
    public static final Hook GETMESSAGE = new Hook(Descriptor.GETMESSAGE);

    /**
     * Hook described by {@link Descriptor#CALLWNDPROC} descriptor.
     */
    public static final Hook CALLWNDPROC = new Hook(Descriptor.CALLWNDPROC);

    /**
     * Hook described by {@link Descriptor#CBT} descriptor.
     */
    public static final CBTHook CBT = new CBTHook();

    /**
     * Hook described by {@link Descriptor#SYSMSGFILTER} descriptor.
     */
    public static final Hook SYSMSGFILTER = new Hook(Descriptor.SYSMSGFILTER);

    /**
     * Hook described by {@link Descriptor#MOUSE} descriptor.
     */
    public static final Hook MOUSE = new Hook(Descriptor.MOUSE);

    /**
     * Hook described by {@link Descriptor#MOUSE_LL} descriptor.
     */
    public static final Hook MOUSE_LL = new LowLevelMouseHook();

    /**
     * Hook described by {@link Descriptor#SHELL} descriptor.
     */
    public static final Hook SHELL = new Hook(Descriptor.SHELL);

    /**
     * Hook described by {@link Descriptor#FOREGROUNDIDLE} descriptor.
     */
    public static final Hook FOREGROUNDIDLE = new Hook(Descriptor.FOREGROUNDIDLE);

    /**
     * Hook described by {@link Descriptor#CALLWNDPROCRET} descriptor.
     */
    public static final Hook CALLWNDPROCRET = new Hook(Descriptor.CALLWNDPROCRET);

    /**
     * Creates a new instance of the hook by a specified hook descriptor.
     *
     * @param descriptor specifies a hook to install.
     */
    Hook(Descriptor descriptor)
    {
        _descriptor = descriptor;
    }

    /**
     * Returns the descriptor of this hook.
     *
     * @return hook descriptor
     */
    public Descriptor getDescriptor()
    {
        return _descriptor;
    }

    /**
     * Verifies if the hook is installed.
     *
     * @return true if the hook is installed.
     */
    public boolean isInstalled()
    {
        return _installed;
    }

    /**
     * Adds a hook event listener.
     *
     * @param listener a hook event listener.
     */
    public void addListener(HookEventListener listener)
    {
        synchronized (_listeners)
        {
            if (!_listeners.contains(listener))
            {
                _listeners.add(listener);
            }
        }
    }

    /**
     * Removes a hook event listener.
     *
     * @param listener a hook event listener.
     */
    public void removeListener(HookEventListener listener)
    {
        synchronized (_listeners)
        {
            _listeners.remove(listener);
        }
    }

    /**
     * Installs the hook.
     */
    public void install()
    {
        if (isInstalled())
            throw new IllegalStateException("Hook is already instaled.");

        _eventLoop = new HookEventLoop();
        _eventsThread = new Thread(_eventLoop);
        _eventsThread.start();

        while (!_eventLoop._messageThreadAlive)
        {
            try
            {
                Thread.sleep(10);
            }
            catch (InterruptedException e)
            {
            }
        }
    }

    /**
     * Uninstalls the hook.
     */
    public void uninstall()
    {
        if (!isInstalled())
            throw new IllegalStateException("Hook is not installed.");

        _eventLoop.uninstall();
        try
        {
            _eventsThread.join();
            _eventsThread = null;
        }
        catch (InterruptedException e)
        {
            LOG.error("", e);
        }
    }

    private static Library getLibrary()
    {
        if (_library == null)
        {
            _library = new Library(Library.NATIVE_CODE);
        }
        return _library;
    }


    /**
     * Notifies listeners about a hook event.
     *
     * @param event event object
     */
    protected void notifyListeners(HookEventObject event)
    {
        List listeners;
        synchronized (_listeners)
        {
            listeners = new LinkedList(_listeners);
        }
        for (Iterator i = listeners.iterator(); i.hasNext(); )
        {
            HookEventListener listener = (HookEventListener) i.next();
            if (listener != null)
            {
                listener.onHookEvent(event);
            }
        }
    }

    /**
     * This class provides a hook events loop.
     */
    private class HookEventLoop implements Runnable
    {
        private EventObject _eventObject;
        private EventObject _eventFeedback;
        private boolean _messageThreadAlive = false;

        /**
         * Installs the hook.
         */
        private void installHook()
        {
            final Function install = getLibrary().getFunction(FUNCTION_InstallHook);
            install.setCallingConvention(Function.CDECL_CALLING_CONVENTION);

            Pointer hookDataPtr = new Pointer(_hooksData);
            install.invoke(hookDataPtr,
                    new Int(_descriptor.getValue()),
                    new AnsiString(_descriptor.getName()));

            _hooksData.setSynchronous(Hook.this, _synchronous);
            _installed = true;
        }

        /**
         * Starts the event loop.
         */
        public void run()
        {
            _eventObject = new EventObject(_descriptor.getName());
            _eventObject.reset();
            _eventFeedback = new EventObject(_descriptor.getName() + ".feedback");
            _eventFeedback.reset();

            installHook();
            _messageThreadAlive = true;
            while (_messageThreadAlive)
            {
                _eventObject.waitFor();
                if (_messageThreadAlive)
                {
                    HookEventObject event = _hooksData.readEvent(_descriptor);
                    notifyListeners(event);
                    _eventObject.reset();
                    _eventFeedback.notifyEvent();
                }
            }

            final Function uninstall = getLibrary().getFunction(FUNCTION_UninstallHook);
            uninstall.setCallingConvention(Function.CDECL_CALLING_CONVENTION);
            uninstall.invoke(null, new UInt(_descriptor.getValue()));

            _eventObject.reset();
            _eventObject.close();
            _eventFeedback.close();
            _installed = false;
        }

        /**
         * Uninstalls the hook.
         */
        private void uninstall()
        {
            _messageThreadAlive = false;
            _eventObject.notifyEvent();
        }
    }

    /**
     * Returns the mode of this hook.
     *
     * @return true if this hook works in synchronous mode; false - in asynchronous mode.
     */
    public boolean isSynchronous()
    {
        if (isInstalled())
        {
            _synchronous = _hooksData.getSynchronous(this);
            return _synchronous;
        }
        else
        {
            return _synchronous;
        }
    }

    /**
     * This method allows to swith between synchronous/asynchronous modes of the hook.<br>
     * <b>Note:</b> synchronous mode may significatly reduce the performance of the system, so use this mode carefully.
     *
     * @param synchronous specifies the mode of the hook; if true then hook works in synchronous mode; false otherwise.
     */
    public void setSynchronous(boolean synchronous)
    {
        _synchronous = synchronous;
        if (isInstalled())
        {
            _hooksData.setSynchronous(this, synchronous);
        }
    }

    /**
     * Specifies events filter for this hook.
     *
     * @param filter events filter
     */
    public void setFilter(EventsFilter filter)
    {
        if (!isInstalled())
        {
            throw new IllegalStateException("Hook is not installed yet");
        }
        _hooksData.setHookFilter(getDescriptor(), filter);
    }

    /**
     * Returns a specified events filter of this hook.
     *
     * @return events filter
     */
    public EventsFilter getFilter()
    {
        return _hooksData.getHookFilter(getDescriptor());
    }
}