package com.jniwrapper.win32.hook;

import com.jniwrapper.*;
import com.jniwrapper.win32.FunctionName;
import com.jniwrapper.win32.Handle;
import com.jniwrapper.win32.MessageLoopThread;
import com.jniwrapper.win32.system.Module;
import com.jniwrapper.win32.ui.User32;

/**
 * Provides base implementation of a low-level hook.
 *
 * @author Serge Piletsky
 */
abstract class LowLevelHook extends Hook
{
    static final int HC_ACTION = 0;
    static final int CONSUME_ACTION = -1;
    private final Handle _hookHandle = new Handle();
    private MessageLoopThread _dispatchThread;
    private HookProc _callback;
    private boolean wasInstalled;

    LowLevelHook(Descriptor descriptor)
    {
        super(descriptor);
        _dispatchThread = new MessageLoopThread("HookDispatchThread" + hashCode());
        NativeResourceCollector.getInstance().addShutdownAction(new Runnable()
        {
            public void run()
            {
                if (isInstalled())
                {
                    uninstall();
                }
            }
        });
    }

    public boolean isInstalled()
    {
        return _dispatchThread.isStarted();
    }

    public void install()
    {
        _dispatchThread.doStart();
        _callback = new HookProc();
        // install a hook in a dispatch thread
        try
        {
            _dispatchThread.doInvokeAndWait(new Runnable()
            {
                public void run()
                {
                    User32 user32 = User32.getInstance();
                    Function setWindowsHook = user32.getFunction(new FunctionName("SetWindowsHookEx").toString());
                    UInt32 threadID = new UInt32();

                    final Module currentModule = Module.getCurrent();
                    setWindowsHook.invoke(_hookHandle, new Parameter[]{
                            new Int(getDescriptor().getValue()),
                            _callback,
                            currentModule,
                            threadID
                    });
                }
            });
        }
        catch (Exception e)
        {
            throw new RuntimeException("Failed to install low level hook", e);
        }
    }

    public void uninstall()
    {
        try
        {
            _dispatchThread.doInvokeAndWait(new Runnable()
            {
                public void run()
                {
                    User32 user32 = User32.getInstance();
                    Function unhook = user32.getFunction("UnhookWindowsHookEx");
                    unhook.invoke(null, _hookHandle);
                }
            });
        }
        catch (Exception e)
        {
            throw new RuntimeException("Failed to uninstall low level hook", e);
        }
        finally
        {
            _dispatchThread.doStop();
            _callback.dispose();
        }
    }

    protected abstract long processEvent(long wParam, long lParam);

    public void addListener(HookEventListener listener)
    {
        super.addListener(listener);
        if (wasInstalled && !isInstalled())
        {
            install();
        }
    }

    public void removeListener(HookEventListener listener)
    {
        super.removeListener(listener);
        wasInstalled = isInstalled();
        if (_listeners.isEmpty() && wasInstalled)
        {
            uninstall();
        }

    }

    private class HookProc extends Callback
    {
        private Int code = new Int();
        private Pointer.Void wParam = new Pointer.Void();
        private Pointer.Void lParam = new Pointer.Void();
        private Pointer.Void lResult = new Pointer.Void();
        private Function CallNextHookEx = User32.getInstance().getFunction("CallNextHookEx");

        public HookProc()
        {
            init(new Parameter[]{code, wParam, lParam}, lResult);
        }

        public void callback()
        {
            long nCode = code.getValue();
            if (nCode < 0)
            {
                CallNextHookEx.invoke(lResult, _hookHandle, code, wParam, lParam);
            }
            else if (nCode == HC_ACTION)
            {
                long postAction = processEvent(wParam.getValue(), lParam.getValue());
                if (postAction == CONSUME_ACTION)
                {
                    lResult.setValue(CONSUME_ACTION);
                }
                else
                {
                    CallNextHookEx.invoke(lResult, _hookHandle, code, wParam, lParam);
                }
            }
            else
            {
                CallNextHookEx.invoke(lResult, _hookHandle, code, wParam, lParam);
            }
        }
    }
}
