# vim:fileencoding=utf-8
# License: GPL v3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
from __python__ import bound_methods, hash_literals

from gettext import gettext as _
from read_book.globals import get_boss, ltr_page_progression, ui_operations
from read_book.settings import opts
from read_book.viewport import get_unit_size_in_pixels, scroll_viewport

HOLD_THRESHOLD = 750  # milliseconds
TAP_THRESHOLD = 8  # pixels
SWIPE_THRESHOLD = 64  # pixels
TAP_LINK_THRESHOLD = 5  # pixels
PINCH_THRESHOLD = 20  # pixels
GESTURE = {k:k for k in v"""[
    'back_zone_tap',
    'forward_zone_tap',
    'control_zone_tap',
    'long_tap',
    'two_finger_tap',
    'pinch_in',
    'pinch_out',

    'flick_inline_backward',
    'flick_inline_forward',
    'flick_block_backward',
    'flick_block_forward',

    'swipe_inline_backward_in_progress',
    'swipe_inline_forward_in_progress',
    'swipe_block_backward_in_progress',
    'swipe_block_forward_in_progress',

    'swipe_inline_backward_hold',
    'swipe_inline_forward_hold',
    'swipe_block_backward_hold',
    'swipe_block_forward_hold',

    'tap',
    'swipe',
    'pinch',
]"""}

def GESTURE_NAMES():
    ans = GESTURE_NAMES.ans
    if not ans:
        GESTURE_NAMES.ans = ans = {
    'back_zone_tap': _('Tap on back zone'),
    'forward_zone_tap': _('Tap on forward zone'),
    'control_zone_tap': _('Tap in the controls zone'),
    'long_tap': _('Long tap'),
    'two_finger_tap': _('Two finger tap'),
    'pinch_in': _('Pinch in'),
    'pinch_out': _('Pinch out'),

    'flick_inline_backward': _('Flick in writing direction, to go back'),
    'flick_inline_forward': _('Flick in writing direction, to go forward'),
    'flick_block_backward': _('Flick perpendicular to writing direction, to go forward'),
    'flick_block_forward': _('Flick perpendicular to writing direction, to go back'),

    'swipe_inline_backward_in_progress': _('Drag finger in writing direction, to go back'),
    'swipe_inline_forward_in_progress': _('Drag finger in writing direction, to go forward'),
    'swipe_block_backward_in_progress': _('Drag finger perpendicular to writing direction, to go back'),
    'swipe_block_forward_in_progress': _('Drag finger perpendicular to writing direction, to go forward'),

    'swipe_inline_backward_hold': _('Drag and hold finger in writing direction, to go back'),
    'swipe_inline_forward_hold': _('Drag and hold finger in writing direction, to go forward'),
    'swipe_block_backward_hold': _('Drag and hold finger perpendicular to writing direction, to go back'),
    'swipe_block_forward_hold': _('Drag and hold finger perpendicular to writing direction, to go forward'),
    }
    return ans

gesture_id = 0

def touch_id(touch):
    # On Safari using touch.identifier as dict key yields a key of "NaN" for some reason
    return touch.identifier + ''

def copy_touch(t):
    now = window.performance.now()
    return {
        'identifier':touch_id(t),
        'page_x':v'[t.pageX]', 'page_y':v'[t.pageY]', 'viewport_x':v'[t.clientX]', 'viewport_y':v'[t.clientY]',
        'active':True, 'mtimes':v'[now]', 'ctime':now, 'is_held':False, 'x_velocity': 0, 'y_velocity': 0
    }


def update_touch(t, touch):
    now = window.performance.now()
    t.mtimes.push(now)
    t.page_x.push(touch.pageX), t.page_y.push(touch.pageY)
    t.viewport_x.push(touch.clientX), t.viewport_y.push(touch.clientY)


def max_displacement(points):
    ans = 0
    first = points[0]
    if not first?:
        return ans
    for p in points:
        delta = abs(p - first)
        if delta > ans:
            ans = delta
    return ans

def interpret_single_gesture(touch, gesture_id):
    max_x_displacement = max_displacement(touch.viewport_x)
    max_y_displacement = max_displacement(touch.viewport_y)
    ans = {'active':touch.active, 'is_held':touch.is_held, 'id':gesture_id, 'start_time': touch.ctime}
    if max(max_x_displacement, max_y_displacement) < TAP_THRESHOLD:
        ans.type = GESTURE.tap
        ans.viewport_x = touch.viewport_x[0]
        ans.viewport_y = touch.viewport_y[0]
        return ans
    if touch.viewport_y.length < 2:
        return ans
    delta_x = abs(touch.viewport_x[-1] - touch.viewport_x[0])
    delta_y = abs(touch.viewport_y[-1] - touch.viewport_y[0])
    max_disp = max(delta_y, delta_x)
    if max_disp > SWIPE_THRESHOLD and min(delta_x, delta_y)/max_disp < 0.35:
        ans.type = GESTURE.swipe
        ans.axis = 'vertical' if delta_y > delta_x else 'horizontal'
        ans.points = pts = touch.viewport_y if ans.axis is 'vertical' else touch.viewport_x
        ans.times = touch.mtimes
        positive = pts[-1] > pts[0]
        if ans.axis is 'vertical':
            ans.direction = 'down' if positive else 'up'
            ans.velocity = touch.y_velocity
        else:
            ans.direction = 'right' if positive else 'left'
            ans.velocity = touch.x_velocity
        return ans
    return ans


def interpret_double_gesture(touch1, touch2, gesture_id):
    ans = {'active':touch1.active or touch2.active, 'is_held':touch1.is_held or touch2.is_held, 'id':gesture_id}
    max_x_displacement1 = max_displacement(touch1.viewport_x)
    max_x_displacement2 = max_displacement(touch2.viewport_x)
    max_y_displacement1 = max_displacement(touch1.viewport_y)
    max_y_displacement2 = max_displacement(touch2.viewport_y)
    if max(max_x_displacement1, max_y_displacement1) < TAP_THRESHOLD and max(max_x_displacement2, max_y_displacement2) < TAP_THRESHOLD:
        ans.type = GESTURE.two_finger_tap
        ans.viewport_x1 = touch1.viewport_x[0]
        ans.viewport_y1 = touch1.viewport_y[0]
        ans.viewport_x2 = touch2.viewport_x[0]
        ans.viewport_y2 = touch2.viewport_y[0]
        return ans
    initial_distance = Math.sqrt((touch1.viewport_x[0] - touch2.viewport_x[0])**2 + (touch1.viewport_y[0] - touch2.viewport_y[0])**2)
    final_distance = Math.sqrt((touch1.viewport_x[-1] - touch2.viewport_x[-1])**2 + (touch1.viewport_y[-1] - touch2.viewport_y[-1])**2)
    distance = abs(final_distance - initial_distance)
    if distance > PINCH_THRESHOLD:
        ans.type = GESTURE.pinch
        ans.direction = 'in' if final_distance < initial_distance else 'out'
        ans.distance = distance
        return ans
    return ans

def element_from_point(x, y):
    # This does not currently support detecting links inside iframes, but since
    # iframes are not common in books, I am going to ignore that for now
    return document.elementFromPoint(x, y)


def find_link(x, y):
    p = element_from_point(x, y)
    while p:
        if p.tagName and p.tagName.toLowerCase() is 'a' and p.hasAttribute('href'):
            return p
        p = p.parentNode


def tap_on_link(gesture):
    for delta_x in [0, TAP_LINK_THRESHOLD, -TAP_LINK_THRESHOLD]:
        for delta_y in [0, TAP_LINK_THRESHOLD, -TAP_LINK_THRESHOLD]:
            x = gesture.viewport_x + delta_x
            y = gesture.viewport_y + delta_y
            link = find_link(x, y)
            if link:
                link.click()
                return True
    return False


class TouchHandler:

    def __init__(self):
        self.ongoing_touches = {}
        self.gesture_id = None
        self.hold_timer = None
        self.handled_tap_hold = False

    def prune_expired_touches(self):
        now = window.performance.now()
        expired = v'[]'
        for tid in self.ongoing_touches:
            t = self.ongoing_touches[tid]
            if t.active:
                if now - t.mtimes[-1] > 3000:
                    expired.push(touch_id(t))
        for tid in expired:
            v'delete self.ongoing_touches[tid]'

    @property
    def has_active_touches(self):
        for tid in self.ongoing_touches:
            t = self.ongoing_touches[tid]
            if t.active:
                return True
        return False

    def reset_handlers(self):
        self.stop_hold_timer()
        self.ongoing_touches = {}
        self.gesture_id = None
        self.handled_tap_hold = False

    def start_hold_timer(self):
        self.stop_hold_timer()
        self.hold_timer = window.setTimeout(self.check_for_hold, 50)

    def stop_hold_timer(self):
        if self.hold_timer is not None:
            window.clearTimeout(self.hold_timer)
            self.hold_timer = None

    def check_for_hold(self):
        if len(self.ongoing_touches) > 0:
            now = window.performance.now()
            found_hold = False
            for touchid in self.ongoing_touches:
                touch = self.ongoing_touches[touchid]
                if touch.active and now - touch.mtimes[-1] > HOLD_THRESHOLD:
                    touch.is_held = True
                    found_hold = True
            if found_hold:
                self.dispatch_gesture()
            self.start_hold_timer()

    def handle_touchstart(self, ev):
        if jstype(ev.cancelable) is not 'boolean' or ev.cancelable:
            ev.preventDefault()
        ev.stopPropagation()
        self.prune_expired_touches()
        for touch in ev.changedTouches:
            self.ongoing_touches[touch_id(touch)] = copy_touch(touch)
            if self.gesture_id is None:
                nonlocal gesture_id
                gesture_id += 1
                self.gesture_id = gesture_id
                self.handled_tap_hold = False
        if len(self.ongoing_touches) > 0:
            self.start_hold_timer()

    def handle_touchmove(self, ev):
        if jstype(ev.cancelable) is not 'boolean' or ev.cancelable:
            ev.preventDefault()
        ev.stopPropagation()
        for touch in ev.changedTouches:
            t = self.ongoing_touches[touch_id(touch)]
            if t:
                update_touch(t, touch)
                self.dispatch_gesture()

    def handle_touchend(self, ev):
        if jstype(ev.cancelable) is not 'boolean' or ev.cancelable:
            ev.preventDefault()
        ev.stopPropagation()
        for touch in ev.changedTouches:
            t = self.ongoing_touches[touch_id(touch)]
            if t:
                t.active = False
                update_touch(t, touch)
        self.prune_expired_touches()
        if not self.has_active_touches:
            self.dispatch_gesture()
            self.reset_handlers()

    def handle_touchcancel(self, ev):
        if jstype(ev.cancelable) is not 'boolean' or ev.cancelable:
            ev.preventDefault()
        ev.stopPropagation()
        for touch in ev.changedTouches:
            tid = touch_id(touch)  # noqa: unused-local
            v'delete self.ongoing_touches[tid]'
        self.gesture_id = None
        self.handled_tap_hold = False

    def dispatch_gesture(self):
        touches = self.ongoing_touches
        num = len(touches)
        gesture = {}
        if num is 1:
            gesture = interpret_single_gesture(touches[Object.keys(touches)[0]], self.gesture_id)
        elif num is 2:
            t = Object.keys(touches)
            gesture = interpret_double_gesture(touches[t[0]], touches[t[1]], self.gesture_id)
        if not gesture?.type:
            return
        self.handle_gesture(gesture)

def inch_in_pixels():
    ans = inch_in_pixels.ans
    if not ans:
        ans = inch_in_pixels.ans = max(2, get_unit_size_in_pixels('in'))
    return ans


class BookTouchHandler(TouchHandler):

    def __init__(self, for_side_margin=None):
        self.for_side_margin = for_side_margin
        TouchHandler.__init__(self)

    def handle_gesture(self, gesture):
        if gesture.type is GESTURE.tap:
            if gesture.is_held:
                if not self.for_side_margin and not self.handled_tap_hold and window.performance.now() - gesture.start_time >= HOLD_THRESHOLD:
                    self.handled_tap_hold = True
                    gesture.type = GESTURE.long_tap
                    get_boss().handle_gesture(gesture)
                return
            if not gesture.active:
                if self.for_side_margin or not tap_on_link(gesture):
                    inch = inch_in_pixels()
                    if gesture.viewport_y < min(100, scroll_viewport.height() / 4):
                        gesture.type = GESTURE.control_zone_tap
                    else:
                        limit = inch
                        # default, books that go left to right.
                        if ltr_page_progression() and not opts.reverse_page_turn_zones:
                            if gesture.viewport_x < min(limit, scroll_viewport.width() / 4):
                                gesture.type = GESTURE.back_zone_tap
                            else:
                                gesture.type = GESTURE.forward_zone_tap
                        # We swap the sizes in RTL mode, so that going to the next page is always the bigger touch region.
                        else:
                            # The "going back" area should not be more than limit units big,
                            # even if 1/4 of the scroll viewport is more than limit units.
                            # Checking against the larger of the width minus the limit units and 3/4 of the width will accomplish that.
                            if gesture.viewport_x > max(scroll_viewport.width() - limit, scroll_viewport.width() * (3/4)):
                                gesture.type = GESTURE.back_zone_tap
                            else:
                                gesture.type = GESTURE.forward_zone_tap
        elif gesture.type is GESTURE.pinch:
            if gesture.active:
                return
            gesture.type = GESTURE.pinch_in if gesture.direction is 'in' else GESTURE.pinch_out
        elif gesture.type is GESTURE.two_finger_tap:
            if gesture.active:
                return
        elif gesture.type is 'swipe':
            backward_dir = 'down' if gesture.axis is 'vertical' else ('right' if ltr_page_progression() else 'left')
            direction = 'backward' if gesture.direction is backward_dir else 'forward'
            inline_dir = 'vertical' if scroll_viewport.vertical_writing_mode else 'horizontal'
            axis = 'inline' if gesture.axis is inline_dir else 'block'
            if gesture.active:
                gesture.type = GESTURE[f'swipe_{axis}_{direction}' + ('_hold' if gesture.is_held else '_in_progress')]
            elif not gesture.is_held:
                gesture.type = GESTURE[f'flick_{axis}_{direction}']
        if self.for_side_margin:
            ui_operations.forward_gesture(gesture)
        else:
            get_boss().handle_gesture(gesture)

    def __repr__(self):
        return 'BookTouchHandler:for_side_margin:' + self.for_side_margin

main_touch_handler = BookTouchHandler()
left_margin_handler = BookTouchHandler('left')
right_margin_handler = BookTouchHandler('right')

def install_handlers(elem, handler, passive):
    options = {'capture': True, 'passive': v'!!passive'}
    elem.addEventListener('touchstart', handler.handle_touchstart, options)
    elem.addEventListener('touchmove', handler.handle_touchmove, options)
    elem.addEventListener('touchend', handler.handle_touchend, options)
    elem.addEventListener('touchcancel', handler.handle_touchcancel, options)

def create_handlers():
    # Safari does not work if we register the handler
    # on window instead of document
    install_handlers(document, main_touch_handler)
    # See https://github.com/kovidgoyal/calibre/pull/1101
    # for why we need touchAction none
    document.body.style.touchAction = 'none'

def reset_handlers():
    main_touch_handler.reset_handlers()

def set_left_margin_handler(elem):
    install_handlers(elem, left_margin_handler)

def set_right_margin_handler(elem):
    install_handlers(elem, right_margin_handler)
