/////////////////////////////////////////////////////////////////////////
// Copyright (c) 2011-2015 by Simon Schneegans
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or (at
// your option) any later version.
//
// This program is distributed in the hope that it will be useful, but
// WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with this program.  If not, see <http://www.gnu.org/licenses/>.
/////////////////////////////////////////////////////////////////////////

namespace GnomePie {

/////////////////////////////////////////////////////////////////////////
/// Globally binds key stroke to given ID's. When one of the bound
/// strokes is invoked, a signal with the according ID is emitted.
/////////////////////////////////////////////////////////////////////////

public class BindingManager : GLib.Object {

    /////////////////////////////////////////////////////////////////////
    /// Called when a stored binding is invoked. The according ID is
    /// passed as argument.
    /////////////////////////////////////////////////////////////////////

    public signal void on_press(string id);

    /////////////////////////////////////////////////////////////////////
    /// A list storing bindings, which are invoked even if Gnome-Pie
    /// doesn't have the current focus
    /////////////////////////////////////////////////////////////////////

    private Gee.List<Keybinding> bindings = new Gee.ArrayList<Keybinding>();

    /////////////////////////////////////////////////////////////////////
    /// Ignored modifier masks, used to grab all keys even if these locks
    /// are active.
    /////////////////////////////////////////////////////////////////////

    private static uint[] lock_modifiers = {
        0,
        Gdk.ModifierType.MOD2_MASK,
        Gdk.ModifierType.LOCK_MASK,
        Gdk.ModifierType.MOD5_MASK,

        Gdk.ModifierType.MOD2_MASK|Gdk.ModifierType.LOCK_MASK,
        Gdk.ModifierType.MOD2_MASK|Gdk.ModifierType.MOD5_MASK,
        Gdk.ModifierType.LOCK_MASK|Gdk.ModifierType.MOD5_MASK,

        Gdk.ModifierType.MOD2_MASK|Gdk.ModifierType.LOCK_MASK|Gdk.ModifierType.MOD5_MASK
    };

    /////////////////////////////////////////////////////////////////////
    /// Some variables to remember which delayed binding was delayed.
    /// When the delay passes without another event indicating that the
    /// Trigger was released, the stored binding will be activated.
    /////////////////////////////////////////////////////////////////////

    private uint32 delayed_count = 0;
    private X.Event? delayed_event = null;
    private Keybinding? delayed_binding = null;

    /////////////////////////////////////////////////////////////////////
    /// Helper class to store keybinding
    /////////////////////////////////////////////////////////////////////

    private class Keybinding {

        public Keybinding(Trigger trigger, string id) {
            this.trigger = trigger;
            this.id = id;
        }

        public Trigger trigger { get; set; }
        public string id { get; set; }
    }

    /////////////////////////////////////////////////////////////////////
    /// C'tor adds the event filter to the root window.
    /////////////////////////////////////////////////////////////////////

    public BindingManager() {
        // init filter to retrieve X.Events
        Gdk.Window rootwin = Gdk.get_default_root_window();
        if(rootwin != null) {
            rootwin.add_filter(event_filter);
        }
    }

    /////////////////////////////////////////////////////////////////////
    /// Binds the ID to the given accelerator.
    /////////////////////////////////////////////////////////////////////

    public void bind(Trigger trigger, string id) {
        if (trigger.key_code != 0) {
            unowned X.Display display = Gdk.X11.get_default_xdisplay();
            X.ID xid = Gdk.X11.get_default_root_xwindow();

            Gdk.error_trap_push();

            // if bound to super key we need to grab MOD4 instead
            // (for whatever reason...)
            var modifiers = prepare_modifiers(trigger.modifiers);

            foreach(uint lock_modifier in lock_modifiers) {
                if (trigger.with_mouse) {
                    display.grab_button(trigger.key_code, modifiers|lock_modifier, xid, false,
                                        X.EventMask.ButtonPressMask | X.EventMask.ButtonReleaseMask,
                                        X.GrabMode.Async, X.GrabMode.Async, xid, 0);
                } else {
                    display.grab_key(trigger.key_code, modifiers|lock_modifier,
                                     xid, false, X.GrabMode.Async, X.GrabMode.Async);
                }
            }

            Gdk.flush();
            Keybinding binding = new Keybinding(trigger, id);
            bindings.add(binding);
            display.flush();
        } else {
            //no key_code: just add the bindind to the list to save optional trigger parameters
            Keybinding binding = new Keybinding(trigger, id);
            bindings.add(binding);
        }
    }

    /////////////////////////////////////////////////////////////////////
    /// Unbinds the accelerator of the given ID.
    /////////////////////////////////////////////////////////////////////

    public void unbind(string id) {
        foreach (var binding in bindings) {
            if (id == binding.id) {
                if (binding.trigger.key_code == 0) {
                    //no key_code: just remove the bindind from the list
                    bindings.remove(binding);
                    return;
                }
                break;
            }
        }

        unowned X.Display display = Gdk.X11.get_default_xdisplay();
        X.ID xid = Gdk.X11.get_default_root_xwindow();

        Gee.List<Keybinding> remove_bindings = new Gee.ArrayList<Keybinding>();
        foreach(var binding in bindings) {
            if(id == binding.id) {

                // if bound to super key we need to ungrab MOD4 instead
                // (for whatever reason...)
                var modifiers = prepare_modifiers(binding.trigger.modifiers);

                foreach(uint lock_modifier in lock_modifiers) {
                    if (binding.trigger.with_mouse) {
                        display.ungrab_button(binding.trigger.key_code, modifiers|lock_modifier, xid);
                    } else {
                        display.ungrab_key(binding.trigger.key_code, modifiers|lock_modifier, xid);
                    }
                }
                remove_bindings.add(binding);
            }
        }

        bindings.remove_all(remove_bindings);
        display.flush();
    }

    /////////////////////////////////////////////////////////////////////
    /// Returns a human readable accelerator for the given ID.
    /////////////////////////////////////////////////////////////////////

    public string get_accelerator_label_of(string id) {
        foreach (var binding in bindings) {
            if (binding.id == id) {
                return binding.trigger.label_with_specials;
            }
        }

        return _("Not bound");
    }

    /////////////////////////////////////////////////////////////////////
    /// Returns the accelerator to which the given ID is bound.
    /////////////////////////////////////////////////////////////////////

    public string get_accelerator_of(string id) {
        foreach (var binding in bindings) {
            if (binding.id == id) {
                return binding.trigger.name;
            }
        }

        return "";
    }

    /////////////////////////////////////////////////////////////////////
    /// Returns whether the pie with the given ID is in turbo mode.
    /////////////////////////////////////////////////////////////////////

    public bool get_is_turbo(string id) {
        foreach (var binding in bindings) {
            if (binding.id == id) {
                return binding.trigger.turbo;
            }
        }

        return false;
    }

    /////////////////////////////////////////////////////////////////////
    /// Returns whether the pie with the given ID opens centered.
    /////////////////////////////////////////////////////////////////////

    public bool get_is_centered(string id) {
        foreach (var binding in bindings) {
            if (binding.id == id) {
                return binding.trigger.centered;
            }
        }

        return false;
    }

    /////////////////////////////////////////////////////////////////////
    /// Returns whether the pie with the given ID is in warp mode.
    /////////////////////////////////////////////////////////////////////

    public bool get_is_warp(string id) {
        foreach (var binding in bindings) {
            if (binding.id == id) {
                return binding.trigger.warp;
            }
        }

        return false;
    }

    /////////////////////////////////////////////////////////////////////
    /// Returns whether the pie with the given ID is auto shaped
    /////////////////////////////////////////////////////////////////////

    public bool get_is_auto_shape(string id) {
        foreach (var binding in bindings) {
            if (binding.id == id) {
                return (binding.trigger.shape == 0);
            }
        }

        return false;
    }

    /////////////////////////////////////////////////////////////////////
    /// Returns the prefered pie shape number
    /////////////////////////////////////////////////////////////////////

    public int get_shape_number(string id) {
        foreach (var binding in bindings) {
            if (binding.id == id) {
                if (binding.trigger.shape == 0)
                    break;  //return default if auto-shaped
                return binding.trigger.shape; //use selected shape
            }
        }

        return 5;   //default= full pie
    }


    /////////////////////////////////////////////////////////////////////
    /// Returns the name ID of the Pie bound to the given Trigger.
    /// Returns "" if there is nothing bound to this trigger.
    /////////////////////////////////////////////////////////////////////

    public string get_assigned_id(Trigger trigger) {
        var second = Trigger.remove_optional(trigger.name);
        if (second != "") {
            foreach (var binding in bindings) {
                var first = Trigger.remove_optional(binding.trigger.name);
                if (first == second) {
                    return binding.id;
                }
            }
        }
        return "";
    }

    /////////////////////////////////////////////////////////////////////
    /// If SUPER_MASK is set in the input, it will be replaced with
    /// MOD4_MASK. For some reason this is required to listen for key
    /// presses of the super button....
    /////////////////////////////////////////////////////////////////////

    private Gdk.ModifierType prepare_modifiers(Gdk.ModifierType mods) {
        if ((mods & Gdk.ModifierType.SUPER_MASK) > 0) {
            mods |= Gdk.ModifierType.MOD4_MASK;
            mods = mods & ~ Gdk.ModifierType.SUPER_MASK;
        }

        return mods & ~lock_modifiers[7];
    }

    /////////////////////////////////////////////////////////////////////
    /// Event filter method needed to fetch X.Events.
    /////////////////////////////////////////////////////////////////////

    private Gdk.FilterReturn event_filter(Gdk.XEvent gdk_xevent, Gdk.Event gdk_event) {

        #if VALA_0_16 || VALA_0_17
            X.Event* xevent = (X.Event*) gdk_xevent;
        #else
            void* pointer = &gdk_xevent;
            X.Event* xevent = (X.Event*) pointer;
        #endif

        if(xevent->type == X.EventType.KeyPress) {
            foreach(var binding in bindings) {

                // remove NumLock, CapsLock and ScrollLock from key state
                var event_mods = prepare_modifiers((Gdk.ModifierType)xevent.xkey.state);
                var bound_mods = prepare_modifiers(binding.trigger.modifiers);

                if(xevent->xkey.keycode == binding.trigger.key_code &&
                   event_mods == bound_mods) {

                    if (binding.trigger.delayed) {
                        this.activate_delayed(binding, *xevent);
                    } else {
                        on_press(binding.id);
                    }
                }
            }
         }
         else if(xevent->type == X.EventType.ButtonPress) {
            foreach(var binding in bindings) {

                // remove NumLock, CapsLock and ScrollLock from key state
                var event_mods = prepare_modifiers((Gdk.ModifierType)xevent.xbutton.state);
                var bound_mods = prepare_modifiers(binding.trigger.modifiers);

                if(xevent->xbutton.button == binding.trigger.key_code &&
                   event_mods == bound_mods) {

                    if (binding.trigger.delayed) {
                        this.activate_delayed(binding, *xevent);
                    } else {
                        on_press(binding.id);
                    }
                }
            }
         }
         else if(xevent->type == X.EventType.ButtonRelease || xevent->type == X.EventType.KeyRelease) {
            this.activate_delayed(null, *xevent);
         }

        return Gdk.FilterReturn.CONTINUE;
    }

    /////////////////////////////////////////////////////////////////////
    /// This method is always called when a trigger is activated which is
    /// delayed. Therefore on_press() is only emitted, when this method
    /// is not called again within 300 milliseconds. Else a fake event is
    /// sent in order to simulate the actual key which has been pressed.
    /////////////////////////////////////////////////////////////////////

    private void activate_delayed(Keybinding? binding , X.Event event) {
        // increase event count, so any waiting event will realize that
        // something happened in the meantime
        var current_count = ++this.delayed_count;

        if (binding == null && this.delayed_event != null) {
            // if the trigger is released and an event is currently waiting
            // simulate that the trigger has been pressed without any inter-
            // ference of Gnome-Pie
            unowned X.Display display = Gdk.X11.get_default_xdisplay();

            // unbind the trigger, else we'll capture that event again ;)
            unbind(delayed_binding.id);

            if (this.delayed_binding.trigger.with_mouse) {
                // simulate mouse click
                XTest.fake_button_event(display, this.delayed_event.xbutton.button, true, 0);
                display.flush();

                XTest.fake_button_event(display, this.delayed_event.xbutton.button, false, 0);
                display.flush();

            } else {
                // simulate key press
                XTest.fake_key_event(display, this.delayed_event.xkey.keycode, true, 0);
                display.flush();

                XTest.fake_key_event(display, this.delayed_event.xkey.keycode, false, 0);
                display.flush();
            }

            // bind it again
            bind(delayed_binding.trigger, delayed_binding.id);

            this.delayed_binding = null;
            this.delayed_event = null;

        } else if (binding != null) {
            // if the trigger has been pressed, store it and wait for any interuption
            // within the next 300 milliseconds
            this.delayed_event = event;
            this.delayed_binding = binding;

            Timeout.add(300, () => {
                // if nothing has been pressed in the meantime
                if (current_count == this.delayed_count) {
                    this.delayed_binding = null;
                    this.delayed_event = null;
                    on_press(binding.id);
                }
                return false;
            });
        }
    }
}

}