import asyncio
import types
from typing import Any, Set, Optional, Callable, TYPE_CHECKING

from .variables import VariableTracker, ExpressionEvaluator, WatchExploder
from .output import OutputHandler
from .utils import Timer, get_source_line

if TYPE_CHECKING:
    from . import AsyncSnooper


class AsyncFrameWatcher:
    def __init__(
        self,
        handler: OutputHandler,
        snooper: 'AsyncSnooper',
        target_code_objects: Set,
        depth: int,
        watch_expressions: tuple,
        watch_explode_expressions: tuple,
    ):
        self.handler = handler
        self.snooper = snooper
        self.target_code_objects = target_code_objects
        self.max_depth = depth
        self.current_depth = 0

        self.var_tracker = VariableTracker()
        self.expr_evaluator = ExpressionEvaluator(watch_expressions)
        self.exploder = WatchExploder(watch_explode_expressions)
        self.timer = Timer()

    def trace(self, frame: types.FrameType, event: str, arg: Any) -> Optional[Callable]:
        """ trace and callback"""
        code_obj = frame.f_code
        is_target = code_obj in self.target_code_objects

        if not is_target:
            return None

        if self.current_depth > self.max_depth * 2:
            return None

        # handle event
        if event == 'call':
            self.current_depth += 1
            return self._handle_call(frame)

        elif event == 'line':
            return self._handle_line(frame)

        elif event == 'return':
            result = self._handle_return(frame, arg)
            if self.current_depth > 0:
                self.current_depth -= 1
            return result

        elif event == 'exception':
            return self._handle_exception(frame, arg)

        return self.trace

    def _handle_call(self, frame: types.FrameType) -> Optional[Callable]:
        """call、 handle func enter and"""
        self.handler.increase_indent()
        lineno = frame.f_lineno
        filename = frame.f_code.co_filename
        func_name = frame.f_code.co_name

        self.handler.write_line(
            event='call',
            filename=filename,
            lineno=lineno,
            func_name=func_name,
        )

        # show ori var
        if frame.f_locals:
            self.var_tracker.previous_vars.clear()
            self.var_tracker.update(frame.f_locals)

            new_vars = self.var_tracker.get_new_vars()
            for name, value in new_vars.items():
                if not name.startswith('__'):
                    self.handler.write_var(var_type='var', name=name, value=value)

        return self.trace

    def _handle_line(self, frame: types.FrameType) -> Optional[Callable]:
        """ handle user code line """
        filename = frame.f_code.co_filename
        lineno = frame.f_lineno
        func_name = frame.f_code.co_name
        source_line = get_source_line(filename, lineno)

        self.handler.write_line(
            event='line',
            filename=filename,
            lineno=lineno,
            func_name=func_name,
            source_line=source_line,
        )

        # update var
        self.var_tracker.update(frame.f_locals)

        # new var
        for name, value in self.var_tracker.get_new_vars().items():
            if not name.startswith('__'):
                self.handler.write_var(var_type='var', name=name, value=value)

        # modified var
        for name, value in self.var_tracker.get_modified_vars().items():
            if not name.startswith('__'):
                self.handler.write_var(var_type='modified', name=name, value=value)

        # watch var
        expr_results = self.expr_evaluator.evaluate_all(frame.f_globals, frame.f_locals)
        for expr, (changed, value) in expr_results.items():
            if changed:
                self.handler.write_var(var_type='watch', name=expr, value=value)

        # show var
        exploded = self.exploder.explode(frame.f_globals, frame.f_locals)
        for path, value in exploded.items():
            self.handler.write_var(var_type='watch', name=path, value=value)

        return self.trace

    def _handle_return(self, frame: types.FrameType, arg: Any) -> Optional[Callable]:
        """ handle func return """
        lineno = frame.f_lineno
        filename = frame.f_code.co_filename
        func_name = frame.f_code.co_name

        # filter if coroutine await
        if _is_coroutine_internal_return(arg):
            self.handler.decrease_indent()
            return self.trace

        self.handler.write_line(
            event='return',
            filename=filename,
            lineno=lineno,
            func_name=func_name,
        )

        # show res
        if arg is not None and not _is_internal_object(arg):
            self.handler.write_var(var_type='watch', name='return', value=arg)

        # show elapsed time
        elapsed = self.timer.elapsed()
        self.handler.write_elapsed(elapsed)
        self.handler.decrease_indent()

        return self.trace

    def _handle_exception(self, frame: types.FrameType, arg: Any) -> Optional[Callable]:
        """ handle exc """
        exc_type, exc_value, exc_traceback = arg

        # filter await StopIteration
        if exc_type is StopIteration:
            return self.trace

        # filter asyncio exc
        if exc_type.__name__ in ('CancelledError', 'TimeoutError'):
            return self.trace

        lineno = frame.f_lineno
        filename = frame.f_code.co_filename
        func_name = frame.f_code.co_name

        self.handler.write_line(
            event='exception',
            filename=filename,
            lineno=lineno,
            func_name=func_name,
        )

        self.handler.write_exception(
            exc_type=exc_type.__name__,
            exc_value=str(exc_value),
        )

        return self.trace


def _is_coroutine_internal_return(arg: Any) -> bool:
    if arg is None:
        return False

    if asyncio.isfuture(arg):
        return True

    try:
        return arg.__class__.__name__ in ('Future', 'Task', '_GatheringFuture')
    except AttributeError:
        return False


def _is_internal_object(obj: Any) -> bool:
    if obj is None:
        return True

    if asyncio.iscoroutine(obj) or asyncio.isfuture(obj):
        return True

    return hasattr(obj, '__await__') and not hasattr(obj, '__iter__')
