package com.jniwrapper.win32.hook;

import com.jniwrapper.win32.Handle;
import com.jniwrapper.win32.Msg;
import com.jniwrapper.win32.ui.Wnd;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

/**
 * Provides the functionality for configuring and filtering hook events.
 */
public class EventsFilter
{
    public static final int MAX_RANGES = 30;
    public static final int MAX_WINDOWS = 10;

    private List/*<Range>*/ ranges = new ArrayList/*<Range>*/(MAX_RANGES);
    private List/*<WindowRecord>*/ windows = new ArrayList/*<WindowRecord>*/(MAX_WINDOWS);

    private boolean allowAllEvents = false;
    private boolean allowAllWindows = false;

    /**
     * Defines range of events.
     */
    public final static class Range implements Comparable
    {
        private final int lowerBound;
        private final int upperBound;

        Range(int lowerBound, Integer upperBound)
        {
            if (upperBound.intValue() < lowerBound)
            {
                throw new IllegalArgumentException("Upper bound cannot be less than lower bound");
            }
            this.lowerBound = lowerBound;
            this.upperBound = upperBound.intValue();
        }

        public Range(int lowerBound, int upperBound)
        {
            this(lowerBound, new Integer(upperBound));
        }

        public Range(int lowerBound)
        {
            this(lowerBound, new Integer(lowerBound));
        }

        public int getLowerBound()
        {
            return lowerBound;
        }

        public int getUpperBound()
        {
            return upperBound;
        }

        public boolean equals(Object o)
        {
            if (this == o) return true;
            if (!(o instanceof Range)) return false;
            Range that = (Range) o;
            return lowerBound == that.lowerBound && lowerBound == that.lowerBound;
        }

        public int hashCode()
        {
            int result = lowerBound;
            result = 31 * result + lowerBound;
            return result;
        }

        public int compareTo(Object o)
        {
            Range anotherRange = (Range) o;
            return getLowerBound() - anotherRange.getLowerBound();
        }

        public String toString()
        {
            StringBuffer buffer = new StringBuffer("Range[");
            if (getUpperBound() == getLowerBound())
            {
                buffer.append(getLowerBound());
            }
            else
            {
                buffer.append(getLowerBound()).append("..").append(getUpperBound());
            }
            buffer.append(']');
            return buffer.toString();
        }

        private boolean isInRange(int value)
        {
            int upperBound = getUpperBound();
            if (upperBound < Integer.MAX_VALUE)
            {
                upperBound++;
            }
            return getLowerBound() <= value && value <= upperBound;
        }

        public boolean overlapsWith(Range anotherRange)
        {
            if (anotherRange == null)
            {
                throw new NullPointerException("Invalid range");
            }
            int bound = anotherRange.getLowerBound();
            return isInRange(bound);
        }

        public Range concatenate(Range anotherRange)
        {
            if (anotherRange == null)
            {
                throw new NullPointerException("Invalid range");
            }

            int lowerBound = Math.min(this.getLowerBound(), anotherRange.getLowerBound());
            int upperBownd = Math.max(this.getUpperBound(), anotherRange.getUpperBound());

            return new Range(lowerBound, upperBownd);
        }

        public Range[] detach(Range anotherRange)
        {
            if (anotherRange == null)
            {
                throw new NullPointerException("Invalid range");
            }

            Range result[];
            if (anotherRange.getUpperBound() >= getUpperBound())
            {
                if (getLowerBound() == anotherRange.getLowerBound())
                {
                    return null;
                }
                else
                {
                    result = new Range[]{new Range(getLowerBound(), anotherRange.getLowerBound() - 1)};
                }
            }
            else
            {
                result = new Range[]{
                        new Range(getLowerBound(), anotherRange.getLowerBound() - 1),
                        new Range(anotherRange.getUpperBound() + 1, getUpperBound())
                };
            }
            return result;
        }
    }

    /**
     * Defines window events.
     */
    public static class WindowRecord
    {
        private Wnd wnd;
        private boolean includeChildWindows;

        WindowRecord(Wnd wnd, boolean includeChildWindows)
        {
            this.wnd = wnd;
            this.includeChildWindows = includeChildWindows;
        }

        WindowRecord(Wnd wnd)
        {
            this(wnd, false);
        }

        public Wnd getWnd()
        {
            return wnd;
        }

        public boolean isIncludeChildWindows()
        {
            return includeChildWindows;
        }

        public boolean equals(Object o)
        {
            if (this == o) return true;
            if (!(o instanceof WindowRecord))
            {
                return false;
            }
            WindowRecord that = (WindowRecord) o;
            return wnd.equals(that.wnd);
        }

        public int hashCode()
        {
            int result = wnd.hashCode();
            result = 31 * result + (includeChildWindows ? 1 : 0);
            return result;
        }
    }

    /**
     * Constructs a default events filter, does not listen to any window and events.
     */
    public EventsFilter()
    {
        setAllowAllWindows(false);
        setAllowAllEvents(false);
    }

    private void addRange(Range range)
    {
        if (ranges.size() == MAX_RANGES)
        {
            throw new RuntimeException("Reached the limit: " + MAX_RANGES + " of suppored ranges");
        }

        if (allowAllEvents)
        {
            ranges.clear();
            allowAllEvents = false;
        }

        for (int i = 0; i < ranges.size(); i++)
        {
            Range aRange = (Range) ranges.get(i);
            boolean rangesAreOverlapped = aRange.overlapsWith(range) || range.overlapsWith(aRange);
            if (rangesAreOverlapped)
            {
                ranges.remove(aRange);
                addRange(aRange.concatenate(range));
                return;
            }
        }
        ranges.add(range);
        Collections.sort(ranges);
    }

    private void removeRange(Range range)
    {
        for (int i = 0; i < ranges.size(); i++)
        {
            Range aRange = (Range) ranges.get(i);
            boolean rangesAreOverlapped = aRange.overlapsWith(range);
            if (rangesAreOverlapped)
            {
                ranges.remove(aRange);
                Range[] ranges = aRange.detach(range);
                if (ranges != null)
                {
                    for (int index = 0; index < ranges.length; index++)
                    {
                        addRange(ranges[index]);
                    }
                    return;
                }
            }
            rangesAreOverlapped = range.overlapsWith(aRange);
            if (rangesAreOverlapped)
            {
                ranges.remove(aRange);
                Collections.sort(ranges);
                return;
            }
        }
    }

    /**
     * Adds an individual event ID to this filer.
     *
     * @param eventID specifies an individual event ID
     * @return this filter
     */
    public EventsFilter addEvent(int eventID)
    {
        addRange(new Range(eventID));
        return this;
    }

    /**
     * Removes the specified event ID from this filer.
     *
     * @param eventID specifies an individual event ID to remove
     * @return this filter
     */
    public EventsFilter removeEvent(int eventID)
    {
        removeRange(new Range(eventID));
        return this;
    }

    /**
     * Adds the set of individual event IDs to this filter.
     *
     * @param eventIDs array of event IDs
     * @return this filter
     */
    public EventsFilter addEvents(int[] eventIDs)
    {
        if (eventIDs == null)
        {
            throw new NullPointerException("Invalid eventIDs");
        }
        if (eventIDs.length == 0)
        {
            return this;
        }
        for (int i = 0; i < eventIDs.length; i++)
        {
            addEvent(eventIDs[i]);
        }
        return this;
    }

    /**
     * Removes the set of specified event IDs from this filer.
     *
     * @param eventIDs specifies the set of event IDs to remove
     * @return this filter
     */
    public EventsFilter removeEvents(int[] eventIDs)
    {
        if (eventIDs == null)
        {
            throw new NullPointerException("Invalid eventIDs");
        }
        for (int i = 0; i < eventIDs.length; i++)
        {
            removeEvent(eventIDs[i]);
        }
        return this;
    }

    /**
     * Adds the range of event IDs.
     *
     * @param lowerBound lower event ID value
     * @param upperBound upper event ID value
     * @return this filter
     */
    public EventsFilter addEventsRange(int lowerBound, int upperBound)
    {
        addRange(new Range(lowerBound, upperBound));
        return this;
    }

    /**
     * Removes the range of events from this filer.
     *
     * @param lowerBound lower event ID value
     * @param upperBound upper event ID value
     * @return this filter
     */
    public EventsFilter removeEventsRange(int lowerBound, int upperBound)
    {
        removeRange(new Range(lowerBound, upperBound));
        return this;
    }

    /**
     * Add the range of keyboard events [WM_KEYFIRST..WM_KEYLAST]
     *
     * @return this filter
     */
    public EventsFilter addKeyboardEventsRange()
    {
        return addEventsRange(Msg.WM_KEYFIRST, Msg.WM_KEYLAST);
    }

    /**
     * Removes the range of keyboard events [WM_KEYFIRST..WM_KEYLAST]
     *
     * @return this filter
     */
    public EventsFilter removeKeyboardEventsRange()
    {
        return removeEventsRange(Msg.WM_KEYFIRST, Msg.WM_KEYLAST);
    }

    /**
     * Add the range of mouse events [WM_MOUSEFIRST..WM_MOUSELAST]
     *
     * @return this filter
     */
    public EventsFilter addMouseEventsRange()
    {
        return addEventsRange(Msg.WM_MOUSEFIRST, Msg.WM_MOUSELAST);
    }

    /**
     * Removes the range of mouse events [WM_MOUSEFIRST..WM_MOUSELAST]
     *
     * @return this filter
     */
    public EventsFilter removeMouseEventsRange()
    {
        return removeEventsRange(Msg.WM_MOUSEFIRST, Msg.WM_MOUSELAST);
    }

    /**
     * Add a window handle.
     *
     * @param wnd specifies a window handle to add
     * @return this filter
     */
    public EventsFilter addWindow(Wnd wnd)
    {
        return addWindow(wnd, false);
    }

    /**
     * Add a window handle.
     *
     * @param wnd                   specifies a window handle
     * @param listenAllChildWindows specifies whether or not filter should include all child windows as well
     * @return this filter
     */
    public EventsFilter addWindow(Wnd wnd, boolean listenAllChildWindows)
    {
        if (allowAllWindows)
        {
            windows.clear();
            allowAllWindows = false;
        }

        WindowRecord windowRecord = new WindowRecord(wnd, listenAllChildWindows);
        windows.add(windowRecord);
        return this;
    }

    /**
     * Removes a window handle from this filter.
     *
     * @param wnd specifies a window handle to remove
     * @return this filter
     */
    public EventsFilter removeWindow(Wnd wnd)
    {
        windows.remove(new WindowRecord(wnd));
        return this;
    }

    /**
     * Returns the list of spacified ranges.
     *
     * @return list of {@link Range} objects
     */
    public List getRanges()
    {
        return Collections.unmodifiableList(ranges);
    }

    /**
     * Returns the list of specified windows.
     *
     * @return list of {@link WindowRecord} objects
     */
    public List getWindows()
    {
        return Collections.unmodifiableList(windows);
    }

    /**
     * Specifies whether or not this filter allows events from all windows.
     *
     * @param value if <code>true</code> filter allows events from all windows; otherwise - events from all windows are being suppressed.
     */
    public void setAllowAllWindows(boolean value)
    {
        if (value)
        {
            removeAllWindows();
            addWindow(new Wnd(Handle.INVALID_HANDLE_VALUE));
        }
        else
        {
            removeAllWindows();
        }
        allowAllWindows = value;
    }

    /**
     * Returns <code>true</code> if this filter is configured to allow events from all windows.
     *
     * @return <code>true</code> if this filter is configured to allow events from all windows, <code>false</code> otherwise
     */
    public boolean getAllowAllWindows()
    {
        return allowAllWindows;
    }

    /**
     * Specifies whether or not this filter allows all events.
     *
     * @param value if <code>true</code> filter allows all events; otherwise - all events are being suppressed.
     */
    public void setAllowAllEvents(boolean value)
    {
        if (value)
        {
            removeAllEvents();
            addEventsRange(1, Integer.MAX_VALUE);
        }
        else
        {
            removeAllEvents();
        }
        allowAllEvents = value;
    }

    /**
     * Returns <code>true</code> if this filter is configured to allow all events.
     *
     * @return <code>true</code> if this filter is configured to allow all events, <code>false</code> otherwise
     */
    public boolean isAllowAllEvents()
    {
        return allowAllEvents;
    }

    /**
     * Removes all previously added windows from this filter.
     *
     * @return this events filter
     */
    public EventsFilter removeAllWindows()
    {
        windows.clear();
        allowAllWindows = false;
        return this;
    }

    /**
     * Removes all previously added events from this filter.
     *
     * @return this events filter
     */
    public EventsFilter removeAllEvents()
    {
        ranges.clear();
        allowAllEvents = false;
        return this;
    }
}
