#! /usr/bin/env python2

# thanks to mdp for many ideas and suggestions

# https://www.mobileread.com/forums/showthread.php?t=272001

from __future__ import print_function

# some assumptions:
#
#   device connected to host (via USB only?) that has adb installed
#   device has 'ok'ed being connected to by adb (developer options + dialog)
#   only one android device connected
#   host display is using 'mirrored' mode - 'extended' not supported (yet?)

# python requirements
#
#   python 2.x
#   PyUserInput - https://pypi.python.org/pypi/PyUserInput
#   # if profiling desired (and see near end of code):
#   yappi - https://pypi.python.org/pypi/yappi

# add/tweak relevant devices and resolutions to 'device_res' below
#
# to determine the model to use as a key (e.g. Max2) below, try:
#
#   adb shell getprop ro.product.board
#
# with the device connected via USB
device_res = {"Max2": (2200, 1650),
              "Note": (1872, 1404)}
      
# https://gist.github.com/scottgwald/6862517
# from http://stackoverflow.com/questions/11524586/accessing-logcat-from-android-via-python
import Queue
import subprocess
import threading
from datetime import datetime, timedelta
import time
import re
import sys
from pymouse import PyMouse

id_str = "OnyxMonitEv"

# XXX: simplify if necessary
line_re = \
  re.compile("^((\d+)-(\d+))\s+" +
             "(?P<time>(\d+):(\d+):(\d+)\.(\d+))\s+" +
             "(\d+)\s+(\d+)\s+" +
             "(\S+)\s+" +
             id_str + ":\s+" +
             "T:(?P<touch>\d+)\|" +
             "(?P<x>\d+),(?P<y>\d+).*")

time_fmt = "%H:%M:%S.%f"

dbl_click_delta = timedelta(milliseconds=1000)
dbl_click_res = 50

class AsyncFileReader(threading.Thread):
    '''
    Helper class to implement asynchronous reading of a file
    in a separate thread. Pushes read lines on a queue to
    be consumed in another thread.
    '''

    def __init__(self, fd, queue):
        assert isinstance(queue, Queue.Queue)
        assert callable(fd.readline)
        threading.Thread.__init__(self)
        self._fd = fd
        self._queue = queue

    def run(self):
        '''The body of the thread: read lines and put them on the queue.'''
        for line in iter(self._fd.readline, ''):
            self._queue.put(line)

    def eof(self):
        '''Check whether there is no more content to expect.'''
        return not self.is_alive() and self._queue.empty()

def guess_model():
    return \
        subprocess.check_output(['adb', 'shell',
                                 'getprop', 'ro.product.board']).rstrip()

def get_device_res():
    model = guess_model()
    if model in device_res.keys():
        return device_res[model]
    else:
        #print("unexpected model guess: " + model, file=sys.stderr)
        raise ValueError("Unxpected model: ", model)

def get_mouse_and_factors():
    (device_x, device_y) = get_device_res()
        
    mouse = PyMouse()
    (host_x, host_y) = mouse.screen_size()

    return (mouse,
            host_x / (device_x * 1.0),
            host_y / (device_y * 1.0))
    
def main():
    try:
        (mouse, factor_x, factor_y) = get_mouse_and_factors()

        cmd_list = ['adb', 'logcat',
                    '-v', 'threadtime',   # in case the default changes again
                    '-T', '1',            # show starting at most recent line
                    id_str + ':I', '*:S'] 
        # undocumented: https://stackoverflow.com/a/14837250
        #print(subprocess.list2cmdline(cmd_list), file=sys.stderr)
        process = \
            subprocess.Popen(cmd_list, stdout=subprocess.PIPE)

        stdout_queue = Queue.Queue()
        stdout_reader = AsyncFileReader(process.stdout, stdout_queue)
        stdout_reader.start()

        last_down_tm = datetime.now()
        # XXX: trying to choose unlikely coords
        last_down_x = 100000
        last_down_y = 100000
        
        while not stdout_reader.eof():
            time.sleep(0.01)
            while not stdout_queue.empty():
                line = stdout_queue.get()
                #print(line, file=sys.stderr)
                m = line_re.match(line)
                if m:
                    tm = datetime.strptime(m.group("time"), time_fmt)
                    t = int(m.group("touch"))
                    x = int(m.group("x")) * factor_x
                    y = int(m.group("y")) * factor_y
                    if t == 0:
                        if ((tm - last_down_tm) < dbl_click_delta) and \
                           (abs(x - last_down_x) < dbl_click_res) and \
                           (abs(y - last_down_y) < dbl_click_res):
                            x = last_down_x
                            y = last_down_y
                            mouse.press(x, y)
                        else:
                            mouse.press(x, y)
                            last_down_tm = tm
                            last_down_x = x
                            last_down_y = y
                    elif (t == 1):
                        mouse.release(x, y)
                    else:
                        print("unexpected touch info: " + t, file=sys.stderr)
                        print(line, file=sys.stderr)
    # just to catch the likes of C-c to allow clean up etc.
    except KeyboardInterrupt:
        pass
    except ValueError:
        pass
    finally:
        if 'process' in locals():
            process.kill()

# the yappi stuff below is for profiling

# adapted profiling bits from:
#   https://gist.github.com/kwlzn/42b809558dc825821ddb

# import atexit, yappi

# def init_yappi():
#     yappi.set_clock_type('cpu') # or 'wall' or ...
#     yappi.start() # can pass builtins=True if desired

# def finish_yappi():
#     yappi.stop()
 
#     stats = yappi.get_func_stats()
#     # resulting callgrind.out can be used w/ kcachegrind or friends
#     for stat_type in ['pstat', 'callgrind', 'ystat']:
#         stats.save('{}.out'.format(stat_type), type=stat_type)
 
#     with open('func_stats.out', 'wb') as fh:
#         stats.print_all(out=fh)
 
#     thread_stats = yappi.get_thread_stats()
#     with open('thread_stats.out', 'wb') as fh:
#         thread_stats.print_all(out=fh)

# atexit.register(finish_yappi)
# init_yappi()

# yappi stuff above here is for profiling

if __name__ == '__main__':
    main()
