# -*- coding: utf-8 -*-
import os
import types

import six
from functools import partial

from .exts.cloud_debug_python.module_explorer import GetCodeObjectAtLine, _GetModuleCodeObjects, _GetLineNumbers

from rook.logger import logger
from rook.singleton import singleton_obj
from rook.processor.error import Error

from ..exceptions import RookBdbCodeNotFound, RookBdbSetBreakpointFailed, RookInvalidPositionException, \
    RookSettingBreakpointError

try:
    from . import cdbg_native
    cdbg_native.InitializeModule(None)
except ImportError:
    # Special handling for Google AppEngine (Python 2.7)
    from google.devtools.cdbg.debuglets.python import cdbg_native


class BPStatus(object):
    __slots__ = ["disabled"]

    def __init__(self):
        self.disabled = False

class Bdb(object):
    def __init__(self):
        self.fncache = {}
        self._cookies = {}
        self._bp_status = {}
        self.user_line = None

    def set_trace(self):
        # Not needed
        pass

    def canonic(self, filename):
        if filename[0] == "<" and filename[-1] == ">":
            return filename
        canonic = self.fncache.get(filename)
        if not canonic:
            canonic = os.path.abspath(filename)
            canonic = os.path.normpath(canonic)
            self.fncache[filename] = canonic
        return canonic

    def ignore_current_thread(self):
        # Not needed
        pass

    def set_break(self, item, filename, lineno, aug_id):
        filename = self.canonic(filename)

        if isinstance(item, types.ModuleType):
            self._set_break_module(item, filename, lineno, aug_id)
        elif isinstance(item, types.CodeType):
            # the caller doesn't know if the code object has this line, so verify here
            self._set_break_code_object(item, filename, lineno, aug_id)
        else:
            raise KeyError(type(item))

    def _set_break_module(self, module, filename, lineno, aug_id):
        status, code_object = GetCodeObjectAtLine(module, lineno)
        if not status:
            if hasattr(module, '__file__'):
                logger.debug("CodeNotFound module filename %s", module.__file__)

            for cobj in _GetModuleCodeObjects(module):
                logger.debug("Name: %s", cobj.co_name)
                for cline in _GetLineNumbers(cobj):
                    logger.debug("Name: %s, Line %d", cobj.co_name, cline)

            if code_object == (None, None):
                raise RookBdbCodeNotFound(filename=filename)
            else:
                raise RookInvalidPositionException(filename=filename, line=lineno, alternatives=code_object)

        self._set_break_code_object(code_object, filename, lineno, aug_id)

    def _set_break_code_object(self, code_object, filename, lineno, aug_id):
        # Install the breakpoint
        bp_status = BPStatus()
        callback_partial = self._get_callback_partial(callback, lineno, code_object, bp_status, filename, aug_id)
        error_callback_partial = self._get_callback_partial(error_callback, lineno, code_object, bp_status, filename, aug_id)

        cookie = cdbg_native.SetConditionalBreakpoint(code_object, lineno, None, callback_partial,
                                                      error_callback_partial)

        if cookie < 0:
            raise RookBdbSetBreakpointFailed("%s on line %d" % (code_object.co_name, lineno))

        self._cookies[aug_id] = cookie
        self._bp_status[aug_id] = bp_status

    def _get_callback_partial(self, callback, lineno, code_object, bp_status, filename, aug_id):
        return partial(callback, lineno=lineno,
                user_line=self.user_line,
                callback_object_id=id(
                    code_object),
                bp_status=bp_status,
                filename=filename,
                pid=os.getpid(),
                aug_id=aug_id)

    def clear_break(self, aug_id):
        try:
            cookie = self._cookies[aug_id]
            status = self._bp_status[aug_id]
        except KeyError:
            return

        cdbg_native.ClearConditionalBreakpoint(cookie)
        status.disabled = True
        del self._cookies[aug_id]
        del self._bp_status[aug_id]

    def clear_all_breaks(self):
        for cookie in six.itervalues(self._cookies):
            cdbg_native.ClearConditionalBreakpoint(cookie)

        for status in six.itervalues(self._bp_status):
            status.disabled = True

        self._cookies = {}
        self._bp_status = {}

    def close(self):
        pass


def error_callback(lineno, user_line, callback_object_id, bp_status, filename, pid, aug_id):
    message = "Line: {lineno}, not found in file: {filename}, please contact support.".format(
        lineno=lineno, filename=filename)
    send_error_for_aug(aug_id, message)


def send_error_for_aug(aug_id, message):
    logger.error(message)
    if hasattr(singleton_obj, '_output'):
        exception = RookSettingBreakpointError(message)
        error = Error(exc=exception, message=message)
        singleton_obj._output.send_rule_status(aug_id, 'Error', error)


# This function has been moved outside of the class so that it can be pickled
# safely by cloudpickle (which will pickle any objects referred to by its closure)
# When changing it, take care to avoid using references to anything not imported within the function
def callback(lineno, user_line, callback_object_id, bp_status, filename, pid, aug_id):
    try:
        if bp_status.disabled is True:
            return

        import inspect
        frame = inspect.currentframe().f_back

        callback_was_pickled = callback_object_id != id(frame.f_code) or pid != os.getpid()

        if not callback_was_pickled and frame and user_line:
            user_line(frame, filename, lineno=lineno, aug_id=aug_id)
    except:  # this is very last line of defense in the hook, can't report from here lgtm[py/catch-base-exception]
        pass
