#
# This file is part of Dragonfly.
# (c) Copyright 2007, 2008 by Christo Butcher
# Licensed under the LGPL.
#
#   Dragonfly is free software: you can redistribute it and/or modify it 
#   under the terms of the GNU Lesser General Public License as published 
#   by the Free Software Foundation, either version 3 of the License, or 
#   (at your option) any later version.
#
#   Dragonfly 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 
#   Lesser General Public License for more details.
#
#   You should have received a copy of the GNU Lesser General Public 
#   License along with Dragonfly.  If not, see 
#   <http://www.gnu.org/licenses/>.
#

"""
Mouse action
============================================================================

This section describes the :class:`Mouse` action object.  This type of 
action is used for controlling the mouse cursor and clicking mouse
button.

Below you'll find some simple examples of :class:`Mouse` usage, followed 
by a detailed description of the available mouse events.


Example mouse actions
............................................................................

The following code moves the mouse cursor to the center of the foreground 
window (``(0.5, 0.5)``) and then clicks the left mouse button once 
(``left``)::

    # Parentheses ("(...)") give foreground-window-relative locations.
    # Fractional locations ("0.5", "0.9") denote a location relative to
    #  the window or desktop, where "0.0, 0.0" is the top-left corner
    #  and "1.0, 1.0" is the bottom-right corner.
    action = Mouse("(0.5, 0.5), left")
    action.execute()

The line below moves the mouse cursor to 100 pixels left of the desktop's 
right edge and 250 pixels down from its top edge (``[-100, 250]``), and 
then double clicks the right mouse button (``right:2``)::

    # Square brackets ("[...]") give desktop-relative locations.
    # Integer locations ("1", "100", etc.) denote numbers of pixels.
    # Negative numbers ("-100") are counted from the right-edge or the
    #  bottom-edge of the desktop or window.
    Mouse("[-100, 250], right:2").execute()

The following command drags the mouse from the top right corner of the 
foreground window (``(0.9, 10), left:down``) to the bottom left corner 
(``(25, -0.1), left:up``)::

    Mouse("(0.9, 10), left:down, (25, -0.1), left:up").execute()

The code below moves the mouse cursor 25 pixels right and 25 pixels up
(``<25, -25>``)::

    # Angle brackets ("<...>") move the cursor from its current position
    #  by the given number of pixels.
    Mouse("<25, -25>").execute()


Mouse specification format
............................................................................

The *spec* argument passed to the :class:`Mouse` constructor specifies 
which mouse events will be emulated.  It is a string consisting of one or 
more comma-separated elements.  Each of these elements has one of the 
following possible formats:

Mouse movement actions:

 - location is absolute on the entire desktop:
   ``[`` *number* ``,`` *number* ``]``
 - location is relative to the foreground window:
   ``(`` *number* ``,`` *number* ``)``
 - move the cursor relative to its current position:
   ``<`` *pixels* ``,`` *pixels* ``>``

In the above specifications, the *number* and *pixels* have the 
following meanings:

 - *number* -- can specify a number of pixels or a fraction of
   the reference window or desktop.  For example:

    - ``(10, 10)`` -- 10 pixels to the right and down from the
      foreground window's left-top corner
    - ``(0.5, 0.5)`` -- center of the foreground window

 - *pixels* -- specifies the number of pixels

Mouse button-press action:
   *keyname* [``:`` *repeat*] [``/`` *pause*]

 - *keyname* -- Specifies which mouse button to click:

    - ``left`` -- left mouse button key
    - ``middle`` -- middle mouse button key
    - ``right`` -- right mouse button key

 - *repeat* -- Specifies how many times the button should be clicked:

    - ``0`` -- don't click the button, this is a no-op
    - ``1`` -- normal button click
    - ``2`` -- double-click
    - ``3`` -- triple-click

 - *pause* --
   Specifies how long to pause *after* clicking the button.  The value
   should be an integer giving in hundredths of a second.  For example,
   ``/100`` would mean one second, and ``/50`` half a second.

Mouse button-hold or button-release action:
   *keyname* ``:`` *hold-or-release* [``/`` *pause*]

 - *keyname* -- Specifies which mouse button to click; same as above.

 - *hold-or-release* --
   Specified whether the button will be held down or released:

    - ``down`` -- hold the button down
    - ``up`` -- release the button

 - *pause* --
   Specifies how long to pause *after* clicking the button; same as above.


Mouse class reference
............................................................................

"""

import time
import win32con
import win32gui
from ctypes             import windll, pointer, c_long, c_ulong, Structure
from .sendinput         import MouseInput, make_input_array, send_input_array
from .action_base       import DynStrActionBase, ActionError
from ..windows.window   import Window
from ..windows.monitor  import monitors


#---------------------------------------------------------------------------

class _point_t(Structure):
    _fields_ = [
                ('x',  c_long),
                ('y',  c_long),
               ]

def get_cursor_position():
    point = _point_t()
    result = windll.user32.GetCursorPos(pointer(point))
    if result:  return (point.x, point.y)
    else:       return None

def set_cursor_position(x, y):
    result = windll.user32.SetCursorPos(c_long(int(x)), c_long(int(y)))
    if result:  return False
    else:       return True


#---------------------------------------------------------------------------

class _EventBase(object):

    def execute():
        pass


class _Move(_EventBase):

    def __init__(self, from_left, horizontal, from_top, vertical):
        self.from_left = from_left
        self.horizontal = horizontal
        self.from_top = from_top
        self.vertical = vertical
        _EventBase.__init__(self)

    def _move_relative(self, rectangle):
        if self.from_left:  horizontal = rectangle.x1
        else:               horizontal = rectangle.x2
        if isinstance(self.horizontal, float):
            distance = self.horizontal * rectangle.dx
        else:
            distance = self.horizontal
        horizontal += distance

        if self.from_top:   vertical = rectangle.y1
        else:               vertical = rectangle.y2
        if isinstance(self.vertical, float):
            distance = self.vertical * rectangle.dy
        else:
            distance = self.vertical
        vertical += distance

        self._move_mouse(horizontal, vertical)

    def _move_mouse(self, horizontal, vertical):
        set_cursor_position(horizontal, vertical)


class _MoveWindow(_Move):

    def execute(self, window):
        self._move_relative(window.get_position())


class _MoveScreen(_Move):

    def execute(self, window):
        self._move_relative(monitors[0].rectangle)


class _MoveRelative(_Move):

    def __init__(self, horizontal, vertical):
        _Move.__init__(self, None, None, None, None)
        self.horizontal = horizontal
        self.vertical = vertical

    def execute(self, window):
        position = get_cursor_position()
        if not position:
            raise ActionError("Failed to retrieve cursor position.")
        horizontal = position[0] + self.horizontal
        vertical   = position[1] + self.vertical
        self._move_mouse(horizontal, vertical)


class _Button(_EventBase):

    def __init__(self, *flags):
        _EventBase.__init__(self)
        self._flags = flags

    def execute(self, window):
        zero = pointer(c_ulong(0))
        inputs = [MouseInput(0, 0, 0, flag, 0, zero)
                  for flag in self._flags]
        array = make_input_array(inputs)
        send_input_array(array)


class _Pause(_EventBase):

    def __init__(self, interval):
        _EventBase.__init__(self)
        self._interval = interval

    def execute(self, window):
        time.sleep(self._interval)


#---------------------------------------------------------------------------

class Mouse(DynStrActionBase):
    """ Action that sends mouse events. """

    def __init__(self, spec=None, static=False):
        """
            Arguments:
             - *spec* (*str*) -- the mouse actions to execute
             - *static* (boolean) --
               if *True*, do not dynamically interpret *spec*
               when executing this action

        """
        DynStrActionBase.__init__(self, spec=spec, static=static)

    def _parse_spec(self, spec):
        """ Convert the given *spec* to keyboard events. """
        events = []
        parts = self._split_parts(spec)
        for part in parts:
            handled = False
            for handler in self._handlers:
                try:
                    if handler(self, part, events):
                        handled = True
                        break
                except Exception, e:
                    continue
            if not handled:
                raise ActionError("Invalid mouse spec: %r (in %r)"
                                  % (part, spec))
        return events

    def _parse_position_pair(self, spec):
        parts = spec.split(",")
        if len(parts) != 2:
            raise ValueError("Invalid position pair spec: %r" % spec)

        h_origin, h_value = self._parse_position(parts[0])
        v_origin, v_value = self._parse_position(parts[1])

        return (h_origin, h_value, v_origin, v_value)

    def _process_window_position(self, spec, events):
        if not spec.startswith("(") or not spec.endswith(")"):
            return False
        h_origin, h_value, v_origin, v_value = self._parse_position_pair(spec[1:-1])
        event = _MoveWindow(h_origin, h_value, v_origin, v_value)
        events.append(event)
        return True

    def _process_screen_position(self, spec, events):
        if not spec.startswith("[") or not spec.endswith("]"):
            return False
        h_origin, h_value, v_origin, v_value = self._parse_position_pair(spec[1:-1])
        event = _MoveScreen(h_origin, h_value, v_origin, v_value)
        events.append(event)
        return True

    def _process_relative_position(self, spec, events):
        if not spec.startswith("<") or not spec.endswith(">"):
            return False
        parts = spec[1:-1].split(",")
        if len(parts) != 2:
            return False
        horizontal = int(parts[0])
        vertical   = int(parts[1])
        event = _MoveRelative(horizontal, vertical)
        events.append(event)
        return True

    _button_flags = {
                     "left":   (win32con.MOUSEEVENTF_LEFTDOWN,
                                win32con.MOUSEEVENTF_LEFTUP),
                     "right":  (win32con.MOUSEEVENTF_RIGHTDOWN,
                                win32con.MOUSEEVENTF_RIGHTUP),
                     "middle": (win32con.MOUSEEVENTF_MIDDLEDOWN,
                                win32con.MOUSEEVENTF_MIDDLEUP),
                    }

    def _process_button(self, spec, events):
        parts = spec.split(":", 1)
        button = parts[0].strip()
        if len(parts) == 1:  special = 1
        else:                special = parts[1].strip()

        if button not in self._button_flags:
            return False
        flag_down, flag_up = self._button_flags[button]

        if special == "down":
            event = _Button(flag_down)
        elif special == "up":
            event = _Button(flag_up)
        else:
            try:
                repeat = int(special)
            except ValueError:
                return False
            flag_series = (flag_down, flag_up) * repeat
            event = _Button(*flag_series)

        events.append(event)
        return True

    def _process_pause(self, spec, events):
        if not spec.startswith("/"):
            return False
        interval = float(spec[1:]) / 100
        event = _Pause(interval)
        events.append(event)
        return True

    _handlers = [
                 _process_window_position,
                 _process_screen_position,
                 _process_relative_position,
                 _process_button,
                 _process_pause,
                ]

    def _parse_position(self, spec):
        spec = spec.strip()

        if spec.startswith("-"):  origin = False
        else:                     origin = True

        if spec.find(".") != -1:  value = float(spec)
        else:                     value = int(spec)

        return (origin, value)

    def _split_parts(self, spec):

        delimiters = ["()", "[]", "<>"]

        parts = spec.split(",")
        items = []
        while parts:
            part = parts.pop(0).strip()

            found_delimiter = False
            for begin, end in delimiters:
                if begin not in part:
                    continue

                item = [part]
                while parts:
                    part = parts.pop(0)
                    item.append(part)
                    if end in part:
                        break
                if end not in part:
                    raise Exception("No closing delimiter found for %r" % item[0])

                new_item = ",".join(item)
                found_delimiter = True
                break

            if not found_delimiter:
                new_item = part

            if "/" in new_item:
                before, after = new_item.split("/", 1)
                items.append(before.strip())
                items.append("/" + after.strip())
            else:
                items.append(new_item)

        return items


    #-----------------------------------------------------------------------

    def _execute_events(self, events):
        """ Send events. """
        window = Window.get_foreground()
        for event in events:
            event.execute(window)
