# coding=utf-8

VERSION = 1

import sys
import os
import re
import threading
import traceback
from kivy.logger import Logger
from kivy.app import App
from kivy.clock import Clock
from werkzeug.wrappers import Request, Response
from werkzeug.serving import run_simple
from jsonrpc import JSONRPCResponseManager, dispatcher
from telenium.xpath import XpathParser
from kivy.input.motionevent import MotionEvent
from kivy.input.provider import MotionEventProvider
from kivy.compat import unichr
from itertools import count
from time import time

nextid = count()
telenium_input = None


def kivythread(f):
    def f2(*args, **kwargs):
        ev = threading.Event()
        ev_value = threading.Event()

        def custom_call(dt):
            if f(*args, **kwargs):
                ev_value.set()
            ev.set()

        Clock.schedule_once(custom_call, 0)
        ev.wait()
        return ev_value.is_set()

    return f2


def pick_widget(widget, x, y):
    ret = None
    # try to filter widgets that are not visible (invalid inspect target)
    if (hasattr(widget, 'visible') and not widget.visible):
        return ret
    if widget.collide_point(x, y):
        ret = widget
        x2, y2 = widget.to_local(x, y)
        # reverse the loop - look at children on top first
        for child in reversed(widget.children):
            ret = pick_widget(child, x2, y2) or ret
    return ret


def collide_at(widget, x, y):
    if widget.collide_point(x, y):
        x2, y2 = widget.to_local(x, y)
        have_results = False
        for child in reversed(widget.children):
            for ret in collide_at(child, x2, y2):
                yield ret
                have_results = True
        if not have_results:
            yield widget


class TeleniumMotionEvent(MotionEvent):
    def depack(self, args):
        self.is_touch = True
        self.sx, self.sy = args[:2]
        super(TeleniumMotionEvent, self).depack(args)


class TeleniumInputProvider(MotionEventProvider):
    events = []

    def update(self, dispatch_fn):
        while self.events:
            event = self.events.pop(0)
            dispatch_fn(*event)


def selectAll(selector, root=None):
    if root is None:
        root = App.get_running_app().root.parent
    parser = XpathParser()
    matches = parser.parse(selector)
    matches = matches.execute(root)
    return matches or []


def selectFirst(selector, root=None):
    matches = selectAll(selector, root=root)
    if matches:
        return matches[0]


def rpc_getattr(selector, key):
    widget = selectFirst(selector)
    if widget:
        return getattr(widget, key)


def path_to(widget):
    from kivy.core.window import Window
    root = Window
    if widget.parent is root or widget.parent == widget or not widget.parent:
        return "/{}".format(widget.__class__.__name__)
    return "{}/{}[{}]".format(
        path_to(widget.parent), widget.__class__.__name__,
        widget.parent.children.index(widget))


def rpc_ping():
    return True


def rpc_version():
    return VERSION


def rpc_get_token():
    return os.environ.get("TELENIUM_TOKEN")


@kivythread
def rpc_app_quit():
    App.get_running_app().stop()
    return True


def rpc_app_ready():
    app = App.get_running_app()
    if app is None:
        return False
    if app.root is None:
        return False
    return True


def rpc_select(selector, with_bounds=False):
    if not with_bounds:
        return list(map(path_to, selectAll(selector)))

    results = []
    for widget in selectAll(selector):
        left, bottom = widget.to_window(widget.x, widget.y)
        right, top = widget.to_window(widget.x + widget.width, widget.y + widget.height)
        bounds = (left, bottom, right, top)
        path = path_to(widget)
        results.append((path, bounds))
    return results


def rpc_highlight(selector):
    if not selector:
        results = []
    else:
        try:
            results = rpc_select(selector, with_bounds=True)
        except:
            _highlight([])
            raise
    _highlight(results)
    return results


@kivythread
def _highlight(results):
    from kivy.graphics import Color, Rectangle, Canvas
    from kivy.core.window import Window
    if not hasattr(Window, "_telenium_canvas"):
        Window._telenium_canvas = Canvas()
    _canvas = Window._telenium_canvas

    Window.canvas.remove(_canvas)
    Window.canvas.add(_canvas)

    _canvas.clear()
    with _canvas:
        Color(1, 0, 0, 0.5)
        for widget, bounds in results:
            left, bottom, right, top = bounds
            Rectangle(pos=(left, bottom), size=(right-left, top-bottom))


@kivythread
def rpc_setattr(selector, key, value):
    ret = False
    for widget in selectAll(selector):
        setattr(widget, key, value)
        ret = True
    return ret


@kivythread
def rpc_element(selector):
    if selectFirst(selector):
        return True


@kivythread
def rpc_execute(cmd):
    app = App.get_running_app()
    idmap = {"app": app}
    print("execute", cmd)
    try:
        exec(cmd, idmap, idmap)
    except Exception:
        traceback.print_exc()
        return False
    return True


def rpc_pick(all=False):
    from kivy.core.window import Window
    widgets = []
    ev = threading.Event()

    def on_touch_down(touch):
        root = App.get_running_app().root
        for widget in Window.children:
            if all:
                widgets.extend(list(collide_at(root, touch.x, touch.y)))
            else:
                widget = pick_widget(root, touch.x, touch.y)
                widgets.append(widget)
        ev.set()
        return True

    orig_on_touch_down = Window.on_touch_down
    Window.on_touch_down = on_touch_down
    ev.wait()
    Window.on_touch_down = orig_on_touch_down
    if widgets:
        if all:
            ret = list(map(path_to, widgets))
        else:
            ret = path_to(widgets[0])
        return ret


@kivythread
def rpc_click_on(selector):
    w = selectFirst(selector)
    if w:
        from kivy.core.window import Window
        cx, cy = w.to_window(w.center_x, w.center_y)
        sx = cx / float(Window.width)
        sy = cy / float(Window.height)
        me = TeleniumMotionEvent("telenium",
                                 id=next(nextid),
                                 args=[sx, sy])
        telenium_input.events.append(("begin", me))
        telenium_input.events.append(("end", me))
        return True


@kivythread
def rpc_drag(selector, target, duration):
    from kivy.base import EventLoop
    w1 = selectFirst(selector)
    w2 = selectFirst(target)
    duration = float(duration)
    if w1 and w2:
        from kivy.core.window import Window
        cx1, cy1 = w1.to_window(w1.center_x, w1.center_y)
        sx1 = cx1 / float(Window.width)
        sy1 = cy1 / float(Window.height)

        me = TeleniumMotionEvent("telenium",
                                 id=next(nextid),
                                 args=[sx1, sy1])

        telenium_input.events.append(("begin", me))
        if not duration:
            telenium_input.events.append(("end", me))

        else:
            d = 0
            while d < duration:
                t = time()
                EventLoop.idle()
                dt = time() - t
                # need to compute that ever frame, it could have moved
                cx2, cy2 = w2.to_window(w2.center_x, w2.center_y)
                sx2 = cx2 / float(Window.width)
                sy2 = cy2 / float(Window.height)

                dsx = dt * (sx2 - me.sx) / (duration - d)
                dsy = dt * (sy2 - me.sy) / (duration - d)

                me.sx += dsx
                me.sy += dsy

                telenium_input.events.append(("update", me))
                d += dt

        telenium_input.events.append(("end", me))
        return True


def rpc_send_keycode(keycodes):
    # very hard to get it right, not fully tested and fail proof.
    # just the basics.
    from kivy.core.window import Keyboard
    keys = keycodes.split("+")
    scancode = 0
    key = None
    sym = ""
    modifiers = []
    for el in keys:
        if re.match("^[A-Z]", el):
            lower_el = el.lower()
            # modifier detected ? add it
            if lower_el in ("ctrl", "meta", "alt", "shift"):
                modifiers.append(lower_el)
                continue
            # not a modifier, convert to scancode
            sym = lower_el
            key = Keyboard.keycodes.get(lower_el, 0)
        else:
            # may fail, so nothing would be done.
            try:
                key = int(el)
                sym = unichr(key)
            except:
                traceback.print_exc()
                return False
    _send_keycode(key, scancode, sym, modifiers)
    return True


@kivythread
def _send_keycode(key, scancode, sym, modifiers):
    from kivy.core.window import Window
    print("Telenium: send key key={!r} scancode={} sym={!r} modifiers={}".format(
        key, scancode, sym, modifiers
    ))
    if not Window.dispatch("on_key_down", key, scancode, sym, modifiers):
        Window.dispatch("on_keyboard", key, scancode, sym, modifiers)
    Window.dispatch("on_key_up", key, scancode)
    return True


def register_input_provider():
    global telenium_input
    telenium_input = TeleniumInputProvider("telenium", None)
    from kivy.base import EventLoop
    EventLoop.add_input_provider(telenium_input)


@Request.application
def application(request):
    print("application request", request.data)
    try:
        response = JSONRPCResponseManager.handle(
            request.data, dispatcher)
        print("application response", response)
        print("application response", response.json)
    except Exception as e:
        print("application exception", e)
        raise
    return Response(response.json, mimetype='application/json')


def run_telenium():
    Logger.info("TeleniumClient: Started at 0.0.0.0:9901")
    register_input_provider()

    dispatcher.add_method(rpc_version, "version")
    dispatcher.add_method(rpc_ping, "ping")
    dispatcher.add_method(rpc_get_token, "get_token")
    dispatcher.add_method(rpc_app_quit, "app_quit")
    dispatcher.add_method(rpc_app_ready, "app_ready")
    dispatcher.add_method(rpc_select, "select")
    dispatcher.add_method(rpc_highlight, "highlight")
    dispatcher.add_method(rpc_getattr, "getattr")
    dispatcher.add_method(rpc_setattr, "setattr")
    dispatcher.add_method(rpc_element, "element")
    dispatcher.add_method(rpc_execute, "execute")
    dispatcher.add_method(rpc_pick, "pick")
    dispatcher.add_method(rpc_click_on, "click_on")
    dispatcher.add_method(rpc_drag, "drag")
    dispatcher.add_method(rpc_send_keycode, "send_keycode")

    run_simple("0.0.0.0", 9901, application)


def start(win, ctx):
    Logger.info("TeleniumClient: Start")
    ctx.thread = threading.Thread(target=run_telenium)
    ctx.thread.daemon = True
    ctx.thread.start()


def stop(win, ctx):
    pass