#!/usr/bin/env python
# -*- coding: utf-8 -*-

import codecs
import subprocess, threading, os, os.path
import os
import random
import socket
import jtrace.config
import time
import sys
from sys import platform as _platform

_DEFAULT_ADB_JDWP_CMD_TIME_ = 0.3

class ConfigError(Exception):
    """
    A config error exception
    """
    pass

class ShellException(Exception):
    """
    Shell error exception
    """
    def __init__(self, command, output, status ):
        self.command = command
        self.output = output
        self.status = status
    def __str__(self):
        return "command=%s,status=%s,output=%s" % (str(self.command), str(self.status), str(self.output))

def printout(prefix, data):
    """
    Debug print for shell command
    """
    data = data.rstrip()
    if not data: return ''
    print prefix + data.replace( '\n', '\n' + prefix )

def sh(command, no_echo=True, no_fail=False, no_wait=False):
    """
    Execute a cmd and get the output
    """
    #no_echo=False
    if not no_echo:
        printout( '>>> ', repr( command ) )

    si = None
    if _platform == "win32":
        si = subprocess.STARTUPINFO()
        si.dwFlags |= subprocess.STARTF_USESHOWWINDOW

    process = subprocess.Popen(
        command,
        stdout = subprocess.PIPE,
        #stderr = subprocess.STDOUT,
        #stderr = None,
        stderr =  subprocess.PIPE,
        #stdin = None, ### does not work with pyinstaller
        stdin = subprocess.PIPE,
        #shell = True if isinstance( command, str ) else False,
        shell = False,
        startupinfo = si
    )

    if no_wait: return process

    output, _ = process.communicate( )
    status = process.returncode
    # HJ: print "status=" + str(status)
    if status:
        if not no_echo: printout( '!!! ', output )
        if not no_fail: raise ShellException( command, output, status )
    else:
        if not no_echo: printout( '::: ', output )

    return output.replace('\r\n','\n')

def sh2(command, no_echo=True, no_fail=False, no_wait=False):
    """
    Execute a cmd and get the output with the workaround for android 6.0 jdwp
    """

    #no_echo=False
    if not no_echo:
        printout( '>>> ', repr( command ) )

    si = None
    if _platform == "win32":
        si = subprocess.STARTUPINFO()
        si.dwFlags |= subprocess.STARTF_USESHOWWINDOW

    process = subprocess.Popen(
        command,
        stdout = subprocess.PIPE,
        #stderr = subprocess.STDOUT,
        #stderr = None,
        stderr =  subprocess.PIPE,
        #stdin = None, ### does not work with pyinstaller
        stdin = subprocess.PIPE,
        #shell = True if isinstance( command, str ) else False,
        shell = False,
        startupinfo = si
    )

    if no_wait: return process

    def terminate(process):
        #print "In terminate"
        try:
            process.terminate()
        except:
            pass
    # FIXME: Hack for 6.0 adb jdwp blocks forever
    threading.Timer(_DEFAULT_ADB_JDWP_CMD_TIME_, terminate, args=[process]).start()

    output, _ = process.communicate()
    status = process.returncode
    # HJ: print "status=" + str(status)
    if status:
        if not no_echo: printout( '!!! ', output )
        #if not no_fail: raise ShellException( command, output, status )
    else:
        if not no_echo: printout( '::: ', output )

    return output.replace('\r\n','\n')

def seq(*args):
    """
    Helper to extract the args
    """
    return args

def adb(*args):
    """
    Execute a ADB command
    """
    try:
        return sh(seq("adb", *args))
    except OSError as err:
        raise ConfigError('could not find "adb" from the Android SDK in your PATH')

def adb_retry(*args):
    """
    Execute a ADB command with retry
    """
    nr = 0
    while nr < 3:
        cmd = seq("adb", *args)
        nr += 1
        try:
            return sh(cmd)
        except Exception as err:
            print "Failed to execute cmd=%s, nr=%d, err=%s" % (cmd, nr, err)
            time.sleep(0.5)
            if nr > 3:
                raise
            else:
                print "Retrying cmd=%s, nr=%d" % (cmd, nr)

def adb2(*args):
    """
    Execute a ADB command using sh2
    """
    try:
        return sh2(seq("adb", *args))
    except OSError as err:
        raise ConfigError('could not find "adb" from the Android SDK in your PATH')

def aapt(*args):
    """
    Execute a aapt command
    """
    try:
        return sh(seq("aapt", *args))
    except OSError as err:
        raise ConfigError('could not find "aapt" from the Android SDK in your PATH')

def find_jdwp(sn):
    """
    Find the jdwp pids currently enabled on the device
    """
    # fixme: android 6.0 adb jdwp blocks forever
    # return ['4825']
    ps = adb2('-s', sn, 'jdwp','')
    ps = ps.replace('\r\n', '\n').splitlines()
    return ps

def find_pids_for_pkgs(sn, pkgs):
    """
    Get the pids for the packages
    """
    pkg_pid = {}
    pkg_pid_ret = {}

    try:
        android_version_str = adb('-s', sn, 'shell', 'getprop', 'ro.build.version.release')
        if android_version_str[0].isdigit():
            android_version = float(android_version_str.split(".")[0])
            if android_version < 8.0:
                ps = adb('-s', sn, 'shell', 'ps')
            else:
                ps = adb('-s', sn, 'shell', 'ps', '-A')
        else:
            ps = adb('-s', sn, 'shell', 'ps', '-A')
    except Exception as e:
        print(e)
        print "ps failed"
        return {}

    ps = ps.replace('\r\n', '\n').splitlines()
    head = ps[0]
    ps = (p.split() for p in ps[1:])

    if head.startswith('PID'):
        ps = ((int(p[0]), p[-1]) for p in ps)
    elif head.startswith('USER'):
        ps = ((int(p[1]), p[-1]) for p in ps)
    else:
        raise ConfigError('could not parse "adb shell ps" output')

    ps = list(ps)

    pkg_mainapp_started = []
    for p in ps:
        pid_in_ps = str(p[0])
        pkg_in_ps = p[1]
        for pkg_name in pkgs:
            if pkg_in_ps.startswith(pkg_name) and pkg_in_ps.split(':')[0] == pkg_name:
                if pkg_in_ps == pkg_name:
                    pkg_mainapp_started.append(pkg_name)
                if pkg_pid.has_key(pkg_name):
                    pkg_pid[pkg_name].append(pid_in_ps)
                else:
                    pkg_pid[pkg_name] = [pid_in_ps]

    for pkg_name, pid_list in pkg_pid.iteritems():
        if pkg_name in pkg_mainapp_started:
            pkg_pid_ret[pkg_name] = pid_list
        else:
            #print "Pkg %s is not traced - main app is not started - started pids = %s" % (pkg_name, pid_list)
            pass

    return pkg_pid_ret

def remove_all_forward(sn):
    """
    Remove all the jdwp -> tcp forward
    """
    adb('-s', sn, 'forward', '--remove-all')

def check_used(port):
    """
    Check if the port is used
    """
    # Create a TCP socket
    address = '127.0.0.1'
    s = socket.socket()
    try:
        s.connect((address, port))
        s.close()
        # print "port %s is used" % port
        return True
    except socket.error, e:
        # print "port %s is available" % port
        return False

def alloc_port():
    """
    Allocate a available tcp port from 32000 to 39000 randomly
    """
    port_beg = 32000
    port_end = 39000
    port = random.randint(port_beg, port_end)
    while check_used(port) == True:
        time.sleep(0)
        port = random.randint(port_beg, port_end)
    return port

def flush_to_disk(f):
    """
    Flush a file to disk
    """
    if f is not None:
        try:
            f.flush()
            os.fsync(f.fileno())
        except:
            print "fail to flush"
            pass

def str2fqclass(s):
    """
    Convert a full function path to class name and function name
    """
    i = s.rfind('.')
    if i == -1:
        print("Cannot parse path")
        return '', ''

    method = s[i:][1:]
    classname = 'L' + s[:i].replace('.', '/') + ';'
    return classname, method

def get_trace_configs(config_file=None):
    """
    Get trace configuration entries
    """
    if not config_file:
        config_file = jtrace.config._CONFIG_PATH_
    trace_configs = []
    err_msg = ''
    if not os.path.exists(config_file):
        err_msg = "config does not exist: %s" % config_file
        return err_msg, trace_configs
    line_nr = 0
    with open(config_file) as f:
        for line in f.readlines():
            line_nr = line_nr + 1
            if len(line) >= 3 and line[:3] == codecs.BOM_UTF8:
                print "BOM detected"
                line = line[3:]
            line = line.strip().replace('\r\n', '\n').decode('utf-8')
            if line and line[0] != '#':
                try:
                    config = line.split(' ')
                    while u'' in config:
                        config.remove(u'')
                    if len(config) != 2:
                        print "Config Error", config
                        err_msg = "Line %d error in %s" % (line_nr, config_file)
                        return err_msg, trace_configs
                    else:
                        trace_configs.append((config[0], config[1]))
                except:
                    err_msg = "Line %d error in %s" % (line_nr, config_file)
                    return err_msg, trace_configs

    return err_msg, trace_configs

if __name__ == "__main__":
    print find_jdwp("192.168.56.103:5555")
    print find_pids_for_pkgs("192.168.56.103:5555", ["com.android.calendar"])
