#!/Library/Frameworks/Python.framework/Versions/2.7/Resources/Python.app/Contents/MacOS/Python
# -*- coding: utf-8 -*-
'''
Copyright (C) 2013  Diego Torres Milano
Created on Mar 28, 2013

Culebra helps you create AndroidViewClient scripts generating a working template that can be
modified to suit more specific needs.
                      __    __    __    __
                     /  \  /  \  /  \  /  \ 
____________________/  __\/  __\/  __\/  __\_____________________________
___________________/  /__/  /__/  /__/  /________________________________
                   | / \   / \   / \   / \   \___
                   |/   \_/   \_/   \_/   \    o \ 
                                           \_____/--<
@author: Diego Torres Milano
@author: Jennifer E. Swofford (ascii art snake)

'''

__version__ = '5.3.0'

import re
import sys
import os
import getopt
import warnings
import subprocess
import codecs
from datetime import date

try:
    sys.path.append(os.path.join(os.environ['ANDROID_VIEW_CLIENT_HOME'], 'src'))
except:
    pass

from com.dtmilano.android.viewclient import ViewClient, View


HELP = 'help'
VERBOSE = 'verbose'
IGNORE_SECURE_DEVICE = 'ignore-secure-device'
FORCE_VIEW_SERVER_USE = 'force-view-server-use'
DO_NOT_START_VIEW_SERVER = 'do-not-start-view-server'
DO_NOT_IGNORE_UIAUTOMATOR_KILLED = 'do-not-ignore-uiautomator-killed'
FIND_VIEWS_BY_ID = 'find-views-by-id'
FIND_VIEWS_WITH_TEXT = 'find-views-with-text'
FIND_VIEWS_WITH_CONTENT_DESCRIPTION = 'find-views-with-content-description'
USE_REGEXPS = 'use-regexps'
VERBOSE_COMMENTS = 'verbose-comments'
UNIT_TEST = 'unit-test'
USE_JAR = 'use-jar'
USE_DICTIONARY = 'use-dictionary'
DICTIONARY_KEYS_FROM = 'dictionary-keys-from'
AUTO_REGEXPS = 'auto-regexps'
START_ACTIVITY = 'start-activity'
OUTPUT = 'output'
INTERACTIVE = 'interactive'
WINDOW = 'window'
APPEND_TO_SYS_PATH = 'append-to-sys-path'

USAGE = 'usage: %s [OPTION]... [serialno]'
SHORT_OPTS = 'HVIFSkw:i:t:d:rCUj:D:K:R:a:o:Ap'
LONG_OPTS = [HELP, VERBOSE, IGNORE_SECURE_DEVICE, FORCE_VIEW_SERVER_USE, DO_NOT_START_VIEW_SERVER,
              DO_NOT_IGNORE_UIAUTOMATOR_KILLED,
              WINDOW + '=',
              FIND_VIEWS_BY_ID + '=', FIND_VIEWS_WITH_TEXT + '=', FIND_VIEWS_WITH_CONTENT_DESCRIPTION + '=',
              USE_REGEXPS, VERBOSE_COMMENTS, UNIT_TEST,
              USE_JAR + '=', USE_DICTIONARY + '=', DICTIONARY_KEYS_FROM + '=', AUTO_REGEXPS + '=',
              START_ACTIVITY + '=',
              OUTPUT + '=', INTERACTIVE, APPEND_TO_SYS_PATH]
LONG_OPTS_ARG = {WINDOW: 'WINDOW',
                  FIND_VIEWS_BY_ID: 'BOOL', FIND_VIEWS_WITH_TEXT: 'BOOL', FIND_VIEWS_WITH_CONTENT_DESCRIPTION: 'BOOL',
                  USE_JAR: 'BOOL', USE_DICTIONARY: 'BOOL', DICTIONARY_KEYS_FROM: 'VALUE', AUTO_REGEXPS: 'LIST',
                  START_ACTIVITY: 'COMPONENT',
                  OUTPUT: 'FILENAME'}
OPTS_HELP = {
    'H': 'prints this help',
    'k': 'don\'t ignore UiAutomator killed',
    'w': 'use WINDOW content (default: -1, all windows)',
    'i': 'whether to use findViewById() in script',
    't': 'whether to use findViewWithText() in script',
    'd': 'whether to use findViewWithContentDescription',
    'r': 'use regexps in matches',
    'U': 'generates unit test script',
    'j': 'use jar and appropriate shebang to run script (deprecated)',
    'D': 'use a dictionary to store the Views found',
    'K': 'dictionary keys from: id, text, content-description',
    'R': 'auto regexps (i.e. clock)',
    'a': 'starts Activity before dump',
    'o': 'output filename',
    'A': 'interactive',
    'p': 'append environment variables values to sys.path'
    }

AUTO_REGEXPS_RES = {'clock': re.compile('[012]\d:[0-5]\d')}
SB_NO_JAR = 'no-jar'
SB_JAR = 'jar'
SB_JAR_LINUX = 'jar-linux'
SHEBANG = {
    SB_NO_JAR: '#! /usr/bin/env python',
    SB_JAR: '#! /usr/bin/env shebang monkeyrunner -plugin $ANDROID_VIEW_CLIENT_HOME/bin/androidviewclient-$ANDROID_VIEW_CLIENT_VERSION.jar @!',
    SB_JAR_LINUX: '#! /usr/local/bin/shebang monkeyrunner -plugin $AVC_HOME/bin/androidviewclient-$AVC_VERSION.jar @!'
    }

indent = ''
prefix = ''

def shortAndLongOptions():
    '''
    @return: the list of corresponding (short-option, long-option) tuples
    '''

    short_opts = SHORT_OPTS.replace(':', '')
    if len(short_opts) != len(LONG_OPTS):
        raise Exception('There is a mismatch between short and long options')
    t = tuple(short_opts) + tuple(LONG_OPTS)
    l2 = len(t)/2
    sl = []
    for i in range(l2):
        sl.append((t[i], t[i+l2]))
    return sl

def usage(exitVal=1):
    print >> sys.stderr, USAGE % progname
    print >> sys.stderr, "Try '%s --help' for more information." % progname
    sys.exit(exitVal)

def help():
    print >> sys.stderr, USAGE % progname
    print >> sys.stderr
    print >> sys.stderr, "Options:"
    for so, lo in shortAndLongOptions():
        o = '  -%c, --%s' % (so, lo)
        if lo[-1] == '=':
            o += LONG_OPTS_ARG[lo[:-1]]
        try:
            o = '%-34s %-45s' % (o, OPTS_HELP[so])
        except:
            pass
        print >> sys.stderr, o
    sys.exit(0)

def error(msg, fatal=False):
    print >>sys.stderr, "%s: ERROR: %s" % (progname, msg)
    if fatal:
        sys.exit(1)

def notNull(val, default):
    if val:
        return val
    return default

def printVerboseComments(view):
    '''
    Prints the verbose comments for view.
    '''

    print '\n# class=%s' % view.getClass(),
    try:
        text = view.getText()
        if text:
            u = 'u' if isinstance(text, unicode) else ''
            if '\n' in text:
                text = re.sub(r'\n(.)', r'\n#\1', text)
            print "text=%c'%s'" % (u, text),
    except:
        pass
    try:
        tag = view.getTag()
        if tab != 'null':
            print 'tag=%s' % tag,
    except:
        pass
    print

def variableNameFromIdOrKey(view):
    '''
    Returns a suitable variable name from the id.

    @type view: L{View}
    @param id: the View from where the I{uniqueId} is obtained

    @return: the variable name from the id
    '''

    var = view.variableNameFromId()
    if options[USE_DICTIONARY]:
        return 'views[\'%s\']' % notNull(dictionaryKeyFrom(options[DICTIONARY_KEYS_FROM], view), var)
    else:
        return var

def dictionaryKeyFrom(key, view):
    if key == 'id':
        return view.getUniqueId()
    elif key == 'text':
        return view.getText()
    elif key == 'content-description':
        return view.getContentDescription()
    else:
        raise Exception('Not a valid dictionary key: %s' % key)

def printFindViewWithText(view, useregexp):
    '''
    Prints the corresponding statement.

    @type view: L{View}
    @param view: the View
    '''

    text = view.getText()
    isUnicode = isinstance(text, unicode)
    u = 'u' if isUnicode else ''
    if text:
        var = variableNameFromIdOrKey(view)
        if text.find(u"\n") > 0 or text.find(u"'") > 0:
            # 2 quotes + 1 quote = 3 quotes
            text = "''%s''" % text
        if useregexp:
            text = "re.compile(%c'''%s''')" % (u, text)
        else:
            text = "%c'%s'" % (u, text)
        print '%s = vc.findViewWithTextOrRaise(%s)' % (var, text)
    elif kwargs1[VERBOSE]:
        warnings.warn('View with id=%s has no text' %  view.getUniqueId())

def printFindViewWithContentDescription(view, useregexp):
    '''
    Prints the corresponding statement.

    @type view: L{View}
    @param view: the View
    '''

    contentDescription = view.getContentDescription()
    if contentDescription:
        var = variableNameFromIdOrKey(view)
        if useregexp:
            if options[AUTO_REGEXPS]:
                for r in options[AUTO_REGEXPS]:
                    autoRegexp = AUTO_REGEXPS_RES[r]
                    if autoRegexp.match(contentDescription):
                        contentDescription = autoRegexp.pattern
                        break
            contentDescription = "re.compile(u'''%s''')" % contentDescription
        else:
            contentDescription = "u'''%s'''" % contentDescription
        print '%s = %svc.findViewWithContentDescriptionOrRaise(%s)' % (var, prefix, contentDescription)
    elif kwargs1[VERBOSE]:
        warnings.warn('View with id=%s has no content-description' %  view.getUniqueId())

def printFindViewById(view):
    '''
    Prints the corresponding statement.

    @type view: L{View}
    @param view: the View
    '''

    var = variableNameFromIdOrKey(view)
    _id = view.getId() if view.getId() else view.getUniqueId()
    print '%s = %svc.findViewByIdOrRaise("%s")' % (var, prefix, _id)

def traverseAndPrint(view):
    '''
    Traverses the View tree and prints the corresponding statement.

    @type view: L{View}
    @param view: the View
    '''

    if indent:
        print indent,
    if options[VERBOSE_COMMENTS]:
        printVerboseComments(view)
    if options[FIND_VIEWS_BY_ID]:
        printFindViewById(view)
    if options[FIND_VIEWS_WITH_TEXT]:
        printFindViewWithText(view, options[USE_REGEXPS])
    if options[FIND_VIEWS_WITH_CONTENT_DESCRIPTION]:
        printFindViewWithContentDescription(view, options[USE_REGEXPS])

def str2bool(v):
    return v.lower() in ("yes", "true", "t", "1", "on")

def value2dictionaryKey(v):
    v = v.lower()
    k = ['id', 'text', 'content-description']
    sk = ['i', 't', 'd']
    if v in k:
        return v
    if v in sk:
        return k[sk.index(v)]
    error("Invalid dictionary key: %s" % v)
    usage()

def getShebangJar():
    if options[USE_JAR]:
        import java
        osName = java.lang.System.getProperty('os.name')
        if osName == 'Linux':
            return SHEBANG[SB_JAR_LINUX]
        else:
            return SHEBANG[SB_JAR]
    else:
        return SHEBANG[SB_NO_JAR]

def getWindowOption():
    return options[WINDOW] if isinstance(options[WINDOW], str) and options[WINDOW][0] in '-0123456789' else "'%s'" % options[WINDOW]

# __main__
progname = os.path.basename(sys.argv[0])
try:
    optlist, args = getopt.getopt(sys.argv[1:], SHORT_OPTS, LONG_OPTS)
    sys.argv[1:] = args
except getopt.GetoptError, e:
    error(str(e))
    usage()

kwargs1 = {VERBOSE: False, 'ignoresecuredevice': False}
kwargs2 = {'forceviewserveruse': False, 'startviewserver': True, 'autodump': False, 'ignoreuiautomatorkilled': True}
options = {FIND_VIEWS_BY_ID: True, FIND_VIEWS_WITH_TEXT: False, FIND_VIEWS_WITH_CONTENT_DESCRIPTION: False,
        USE_REGEXPS: False, VERBOSE_COMMENTS: False,
        UNIT_TEST: False, USE_JAR: False, USE_DICTIONARY: False, DICTIONARY_KEYS_FROM: 'id',
        AUTO_REGEXPS: None, START_ACTIVITY: None, OUTPUT: None, INTERACTIVE: False,
        WINDOW: -1, APPEND_TO_SYS_PATH: False}
transform = traverseAndPrint
for o, a in optlist:
    o = o.strip('-')
    if o in ['H', HELP]:
        help()
    elif o in ['V', VERBOSE]:
        kwargs1[VERBOSE] = True
    elif o in ['I', IGNORE_SECURE_DEVICE]:
        kwargs1['ignoresecuredevice'] = True
    elif o in ['F', FORCE_VIEW_SERVER_USE]:
        kwargs2['forceviewserveruse'] = True
    elif o in ['S', DO_NOT_START_VIEW_SERVER]:
        kwargs2['startviewserver'] = False
    elif o in ['k', DO_NOT_IGNORE_UIAUTOMATOR_KILLED]:
        kwargs2['ignoreuiautomatorkilled'] = False
    elif o in ['w', WINDOW]:
         options[WINDOW] = a
    elif o in ['i', FIND_VIEWS_BY_ID]:
        options[FIND_VIEWS_BY_ID] = str2bool(a)
    elif o in ['t', FIND_VIEWS_WITH_TEXT]:
        options[FIND_VIEWS_WITH_TEXT] = str2bool(a)
    elif o in ['d', FIND_VIEWS_WITH_CONTENT_DESCRIPTION]:
        options[FIND_VIEWS_WITH_CONTENT_DESCRIPTION] = str2bool(a)
    elif o in ['r', USE_REGEXPS]:
        options[USE_REGEXPS] = True
    elif o in ['C', VERBOSE_COMMENTS]:
        options[VERBOSE_COMMENTS] = True
    elif o in ['U', UNIT_TEST]:
        #warnings.warn('Not implemented yet: %s' % o)
        options[UNIT_TEST] = True
    elif o in ['j', USE_JAR]:
        options[USE_JAR] = str2bool(a)
    elif o in ['D', USE_DICTIONARY]:
        options[USE_DICTIONARY] = str2bool(a)
    elif o in ['K', DICTIONARY_KEYS_FROM]:
        options[DICTIONARY_KEYS_FROM] = value2dictionaryKey(a)
    elif o in ['R', AUTO_REGEXPS]:
        options[AUTO_REGEXPS] = a.split(',')
        for r in options[AUTO_REGEXPS]:
            if r not in AUTO_REGEXPS_RES:
                error("invalid auto regexp: %s\n" % (r))
                usage()
    elif o in ['a', START_ACTIVITY]:
        options[START_ACTIVITY] = a
    elif o in ['o', OUTPUT]:
        options[OUTPUT] = a
    elif o in ['A', INTERACTIVE]:
        options[INTERACTIVE] = True
    elif o in ['p', APPEND_TO_SYS_PATH]:
        options[APPEND_TO_SYS_PATH] = True

if not (options[FIND_VIEWS_BY_ID] or options[FIND_VIEWS_WITH_TEXT] or options[FIND_VIEWS_WITH_CONTENT_DESCRIPTION]):
    if not options[VERBOSE_COMMENTS]:
        warnings.warn('All printing options disabled. Output will be empty.')
    else:
        warnings.warn('Only verbose comments will be printed')

device, serialno = ViewClient.connectToDeviceOrExit(**kwargs1)
if options[START_ACTIVITY]:
    device.startActivity(component=options[START_ACTIVITY])
vc = ViewClient(device, serialno, **kwargs2)
if options[OUTPUT]:
    sys.stdout = codecs.open(options[OUTPUT], mode='w', encoding='utf-8', errors='replace')
    import stat
    st = os.stat(options[OUTPUT])
    os.chmod(options[OUTPUT], st.st_mode | stat.S_IEXEC)

print '''%s
# -*- coding: utf-8 -*-
\'\'\'
Copyright (C) 2013  Diego Torres Milano
Created on %s by Culebra v%s

                      __    __    __    __
                     /  \  /  \  /  \  /  \ 
____________________/  __\/  __\/  __\/  __\_____________________________
___________________/  /__/  /__/  /__/  /________________________________
                   | / \   / \   / \   / \   \___
                   |/   \_/   \_/   \_/   \    o \ 
                                           \_____/--<
@author: Diego Torres Milano
@author: Jennifer E. Swofford (ascii art snake)
\'\'\'


import re
import sys
import os
''' % (getShebangJar(), date.today(), __version__)

if options[UNIT_TEST]:
    print '''\
import unittest
'''

if options[APPEND_TO_SYS_PATH]:
    print '''
try:
    sys.path.append(os.path.join(os.environ['ANDROID_VIEW_CLIENT_HOME'], 'src'))
except:
    pass
'''

print '''\
from com.dtmilano.android.viewclient import ViewClient
'''

if options[UNIT_TEST]:
    print '''
class CulebraTests(unittest.TestCase):

    kwargs1 = None
    kwargs2 = None
    
    @classmethod
    def setUpClass(cls):
        cls.kwargs1 = %s
        cls.kwargs2 = %s
    
    def setUp(self):
        self.device, self.serialno = ViewClient.connectToDeviceOrExit(**CulebraTests.kwargs1)
''' % (kwargs1, kwargs2),

    if options[START_ACTIVITY]:
        print '''\
        self.device.startActivity(component='%s')
''' % options[START_ACTIVITY],
    
    print '''\
        self.vc = ViewClient(self.device, self.serialno, **CulebraTests.kwargs2)

    def tearDown(self):
        pass

    def preconditions(self):
        return True
        
    def testSomething(self):
        if not self.preconditions():
            self.fail('Preconditions failed')
        self.vc.dump(%s)
    ''' % (getWindowOption())
    
    if options[USE_DICTIONARY]:
        print '''\
        self.views = dict()'''
            
    vc.dump(window=options[WINDOW])
    indent = ' ' * 7
    prefix = 'self.'
    vc.traverse(transform=transform)
    
    print '''
    @staticmethod
    def main():
        unittest.main()


if __name__ == '__main__':
    CulebraTests.main()
'''
else:
    print '''
kwargs1 = %s
device, serialno = ViewClient.connectToDeviceOrExit(**kwargs1)
''' % kwargs1,
    
    if options[START_ACTIVITY]:
        print '''\
device.startActivity(component='%s')
    ''' % options[START_ACTIVITY],
    
    print '''\
kwargs2 = %s
vc = ViewClient(device, serialno, **kwargs2)
vc.dump(window=%s)
''' % (kwargs2, getWindowOption())
    
    if options[USE_DICTIONARY]:
        print '''views = dict()'''
    
    vc.dump(window=options[WINDOW])
    vc.traverse(transform=transform)

if options[INTERACTIVE]:
    import socket
    HOST = 'localhost'
    PORT = 8900
    s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    s.connect((HOST, PORT))
    s.sendall("RECORD EVENTS START\n")
    fin = open("/dev/tty")
    while True:
        print >>sys.stderr, "Reading events..."
        data = s.recv(1024)
        code = ViewClient.excerpt(data)
        exec code
        resp = raw_input("Continue recording events? [Y/n]: ")
        if resp in ['N', 'n']:
            break
    s.sendall("RECORD EVENTS STOP\n")
    s.close()
