# -*- coding: utf-8 -*-
from __future__ import unicode_literals, print_function

import fnmatch
from functools import wraps
import json
import numbers
import sys
import threading
import traceback

import _frida


_Cancellable = _frida.Cancellable


def cancellable(f):
    @wraps(f)
    def wrapper(*args, **kwargs):
        cancellable = kwargs.pop('cancellable', None)
        if cancellable is not None:
            with cancellable:
                return f(*args, **kwargs)

        return f(*args, **kwargs)

    return wrapper


class DeviceManager(object):
    def __init__(self, impl):
        self._impl = impl

    def __repr__(self):
        return repr(self._impl)

    def get_local_device(self, **kwargs):
        return self.get_device_matching(lambda d: d.type == 'local', timeout=0, **kwargs)

    def get_remote_device(self, **kwargs):
        return self.get_device_matching(lambda d: d.type == 'remote', timeout=0, **kwargs)

    def get_usb_device(self, timeout=0, **kwargs):
        return self.get_device_matching(lambda d: d.type == 'usb', timeout, **kwargs)

    def get_device(self, id, timeout=0, **kwargs):
        return self.get_device_matching(lambda d: d.id == id, timeout, **kwargs)

    @cancellable
    def get_device_matching(self, predicate, timeout=0):
        if timeout < 0:
            raw_timeout = -1
        elif timeout == 0:
            raw_timeout = 0
        else:
            raw_timeout = int(timeout * 1000.0)
        return Device(self._impl.get_device_matching(lambda d: predicate(Device(d)), raw_timeout))

    @cancellable
    def enumerate_devices(self):
        return [Device(device) for device in self._impl.enumerate_devices()]

    @cancellable
    def add_remote_device(self, host):
        return Device(self._impl.add_remote_device(host))

    @cancellable
    def remove_remote_device(self, host):
        self._impl.remove_remote_device(host)

    def on(self, signal, callback):
        self._impl.on(signal, callback)

    def off(self, signal, callback):
        self._impl.off(signal, callback)


class Device(object):
    def __init__(self, device):
        self.id = device.id
        self.name = device.name
        self.icon = device.icon
        self.type = device.type

        self._impl = device

    def __repr__(self):
        return repr(self._impl)

    @cancellable
    def get_frontmost_application(self):
        return self._impl.get_frontmost_application()

    @cancellable
    def enumerate_applications(self):
        return self._impl.enumerate_applications()

    @cancellable
    def enumerate_processes(self):
        return self._impl.enumerate_processes()

    @cancellable
    def get_process(self, process_name):
        process_name_lc = process_name.lower()
        matching = [process for process in self._impl.enumerate_processes() if fnmatch.fnmatchcase(process.name.lower(), process_name_lc)]
        if len(matching) == 1:
            return matching[0]
        elif len(matching) > 1:
            raise _frida.ProcessNotFoundError("ambiguous name; it matches: %s" % ", ".join(["%s (pid: %d)" % (process.name, process.pid) for process in matching]))
        else:
            raise _frida.ProcessNotFoundError("unable to find process with name '%s'" % process_name)

    @cancellable
    def enable_spawn_gating(self):
        return self._impl.enable_spawn_gating()

    @cancellable
    def disable_spawn_gating(self):
        return self._impl.disable_spawn_gating()

    @cancellable
    def enumerate_pending_spawn(self):
        return self._impl.enumerate_pending_spawn()

    @cancellable
    def enumerate_pending_children(self):
        return self._impl.enumerate_pending_children()

    @cancellable
    def spawn(self, program, argv=None, envp=None, env=None, cwd=None, stdio=None, **kwargs):
        if not isinstance(program, string_types):
            argv = program
            program = argv[0]
            if len(argv) == 1:
                argv = None

        aux_options = kwargs

        return self._impl.spawn(program, argv, envp, env, cwd, stdio, aux_options)

    @cancellable
    def input(self, target, data):
        self._impl.input(self._pid_of(target), data)

    @cancellable
    def resume(self, target):
        self._impl.resume(self._pid_of(target))

    @cancellable
    def kill(self, target):
        self._impl.kill(self._pid_of(target))

    @cancellable
    def attach(self, target):
        return Session(self._impl.attach(self._pid_of(target)))

    @cancellable
    def inject_library_file(self, target, path, entrypoint, data):
        return self._impl.inject_library_file(self._pid_of(target), path, entrypoint, data)

    @cancellable
    def inject_library_blob(self, target, blob, entrypoint, data):
        return self._impl.inject_library_blob(self._pid_of(target), blob, entrypoint, data)

    @cancellable
    def open_channel(self, address):
        return IOStream(self._impl.open_channel(address))

    def on(self, signal, callback):
        self._impl.on(signal, callback)

    def off(self, signal, callback):
        self._impl.off(signal, callback)

    def _pid_of(self, target):
        if isinstance(target, numbers.Number):
            return target
        else:
            return self.get_process(target).pid


class Session(object):
    def __init__(self, impl):
        self._impl = impl

    def __repr__(self):
        return repr(self._impl)

    @cancellable
    def detach(self):
        self._impl.detach()

    @cancellable
    def enable_child_gating(self):
        self._impl.enable_child_gating()

    @cancellable
    def disable_child_gating(self):
        self._impl.disable_child_gating()

    @cancellable
    def create_script(self, *args, **kwargs):
        return Script(self._impl.create_script(*args, **kwargs))

    @cancellable
    def create_script_from_bytes(self, *args, **kwargs):
        return Script(self._impl.create_script_from_bytes(*args, **kwargs))

    @cancellable
    def compile_script(self, *args, **kwargs):
        return self._impl.compile_script(*args, **kwargs)

    @cancellable
    def enable_debugger(self, *args, **kwargs):
        self._impl.enable_debugger(*args, **kwargs)

    @cancellable
    def disable_debugger(self):
        self._impl.disable_debugger()

    @cancellable
    def enable_jit(self):
        self._impl.enable_jit()

    def on(self, signal, callback):
        self._impl.on(signal, callback)

    def off(self, signal, callback):
        self._impl.off(signal, callback)


class Script(object):
    def __init__(self, impl):
        self.exports = ScriptExports(self)

        self._impl = impl

        self._on_message_callbacks = []
        self._log_handler = self.default_log_handler

        self._pending = {}
        self._next_request_id = 1
        self._cond = threading.Condition()

        impl.on('destroyed', self._on_destroyed)
        impl.on('message', self._on_message)

    def __repr__(self):
        return repr(self._impl)

    @cancellable
    def load(self):
        self._impl.load()

    @cancellable
    def unload(self):
        self._impl.unload()

    @cancellable
    def eternalize(self):
        self._impl.eternalize()

    @cancellable
    def post(self, message, **kwargs):
        raw_message = json.dumps(message)
        self._impl.post(raw_message, **kwargs)

    def on(self, signal, callback):
        if signal == 'message':
            self._on_message_callbacks.append(callback)
        else:
            self._impl.on(signal, callback)

    def off(self, signal, callback):
        if signal == 'message':
            self._on_message_callbacks.remove(callback)
        else:
            self._impl.off(signal, callback)

    def get_log_handler(self):
        return self._log_handler

    def set_log_handler(self, handler):
        self._log_handler = handler

    def default_log_handler(self, level, text):
        if level == 'info':
            print(text, file=sys.stdout)
        else:
            print(text, file=sys.stderr)

    @cancellable
    def _rpc_request(self, *args):
        result = [False, None, None]

        def on_complete(value, error):
            with self._cond:
                result[0] = True
                result[1] = value
                result[2] = error
                self._cond.notify_all()

        def on_cancelled():
            self._pending.pop(request_id, None)
            on_complete(None, None)

        with self._cond:
            request_id = self._next_request_id
            self._next_request_id += 1
            self._pending[request_id] = on_complete

        message = ['frida:rpc', request_id]
        message.extend(args)
        try:
            self.post(message)
        except Exception as e:
            self._pending.pop(request_id, None)
            raise

        cancellable = Cancellable.get_current()
        cancel_handler = cancellable.connect(on_cancelled)
        try:
            with self._cond:
                while not result[0]:
                    self._cond.wait()
        finally:
            cancellable.disconnect(cancel_handler)

        cancellable.raise_if_cancelled()

        if result[2] is not None:
            raise result[2]

        return result[1]

    def _on_rpc_message(self, request_id, operation, params, data):
        if operation in ('ok', 'error'):
            callback = self._pending.pop(request_id, None)
            if callback is None:
                return

            value = None
            error = None
            if operation == 'ok':
                value = params[0] if data is None else data
            else:
                error = RPCException(*params[0:3])

            callback(value, error)

    def _on_destroyed(self):
        while True:
            next_pending = None

            with self._cond:
                pending_ids = list(self._pending.keys())
                if len(pending_ids) > 0:
                    next_pending = self._pending.pop(pending_ids[0])

            if next_pending is None:
                break

            next_pending(None, _frida.InvalidOperationError('script is destroyed'))

    def _on_message(self, raw_message, data):
        message = json.loads(raw_message)

        mtype = message['type']
        payload = message.get('payload', None)
        if mtype == 'log':
            level = message['level']
            text = payload
            self._log_handler(level, text)
        elif mtype == 'send' and isinstance(payload, list) and len(payload) > 0 and payload[0] == 'frida:rpc':
            request_id = payload[1]
            operation = payload[2]
            params = payload[3:]
            self._on_rpc_message(request_id, operation, params, data)
        else:
            for callback in self._on_message_callbacks[:]:
                try:
                    callback(message, data)
                except:
                    traceback.print_exc()


class RPCException(Exception):
    def __str__(self):
        return self.args[2] if len(self.args) >= 3 else self.args[0]


class ScriptExports(object):
    def __init__(self, script):
        self._script = script

    def __getattr__(self, name):
        script = self._script
        js_name = _to_camel_case(name)
        def method(*args, **kwargs):
            return script._rpc_request('call', js_name, args, **kwargs)
        return method


class IOStream(object):
    def __init__(self, impl):
        self._impl = impl

    def __repr__(self):
        return repr(self._impl)

    @property
    def is_closed(self):
        return self._impl.is_closed()

    @cancellable
    def close(self):
        self._impl.close()

    @cancellable
    def read(self, count):
        return self._impl.read(count)

    @cancellable
    def read_all(self, count):
        return self._impl.read_all(count)

    @cancellable
    def write(self, data):
        return self._impl.write(data)

    @cancellable
    def write_all(self, data):
        self._impl.write_all(data)


class Cancellable(object):
    def __init__(self):
        self._impl = _Cancellable()

    def __repr__(self):
        return repr(self._impl)

    @property
    def is_cancelled(self):
        return self._impl.is_cancelled()

    def raise_if_cancelled(self):
        self._impl.raise_if_cancelled()

    @classmethod
    def get_current(cls):
        return _Cancellable.get_current()

    def __enter__(self):
        self._impl.push_current()

    def __exit__(self, *args):
        self._impl.pop_current()

    def connect(self, callback):
        return self._impl.connect(callback)

    def disconnect(self, handler_id):
        self._impl.disconnect(handler_id)

    def cancel(self):
        self._impl.cancel()


def _to_camel_case(name):
    result = ""
    uppercase_next = False
    for c in name:
        if c == '_':
            uppercase_next = True
        elif uppercase_next:
            result += c.upper()
            uppercase_next = False
        else:
            result += c.lower()
    return result


if sys.version_info[0] >= 3:
    string_types = str,
else:
    string_types = basestring,
