from opcode import HAVE_ARGUMENT, EXTENDED_ARG, hasconst, opname, hasname, hasjrel, haslocal, \
    hascompare, hasfree, cmp_op
import dis
import sys
from collections import namedtuple

try:
    xrange
except NameError:
    xrange = range

from _pydevd_bundle.pydevd_constants import IS_PY38_OR_GREATER, IS_PY312_OR_GREATER


class TryExceptInfo(object):

    def __init__(self, try_line, is_finally=False):
        self.try_line = try_line
        self.is_finally = is_finally
        self.except_line = -1
        self.except_bytecode_offset = -1
        self.except_end_line = -1
        self.except_end_bytecode_offset = -1
        self.raise_lines_in_except = []

    def is_line_in_try_block(self, line):
        return self.try_line <= line <= self.except_line

    def is_line_in_except_block(self, line):
        return self.except_line <= line <= self.except_end_line

    def __str__(self):
        lst = [
            '{try:',
            str(self.try_line),
            ' except ',
            str(self.except_line),
            ' end block ',
            str(self.except_end_line),
        ]
        if self.raise_lines_in_except:
            lst.append(' raises: %s' % (', '.join(str(x) for x in self.raise_lines_in_except),))

        lst.append('}')
        return ''.join(lst)

    __repr__ = __str__


class ReturnInfo(object):

    def __init__(self, return_line):
        self.return_line = return_line

    def __str__(self):
        return '{return: %s}' % (self.return_line,)

    __repr__ = __str__


def _get_line(op_offset_to_line, op_offset, firstlineno, search=False):
    op_offset_original = op_offset
    while op_offset >= 0:
        ret = op_offset_to_line.get(op_offset)
        if ret is not None:
            return ret - firstlineno
        if not search:
            return ret
        else:
            op_offset -= 1
    raise AssertionError('Unable to find line for offset: %s.Info: %s' % (
        op_offset_original, op_offset_to_line))


def debug(s):
    pass


_Instruction = namedtuple('_Instruction', 'opname, opcode, starts_line, argval, is_jump_target, offset')


def _iter_as_bytecode_as_instructions_py2(co):
    code = co.co_code
    op_offset_to_line = dict(dis.findlinestarts(co))
    labels = set(dis.findlabels(code))
    bytecode_len = len(code)
    i = 0
    extended_arg = 0
    free = None

    op_to_name = opname

    while i < bytecode_len:
        c = code[i]
        op = ord(c)
        is_jump_target = i in labels

        curr_op_name = op_to_name[op]
        initial_bytecode_offset = i

        i = i + 1
        if op < HAVE_ARGUMENT:
            yield _Instruction(curr_op_name, op, _get_line(op_offset_to_line, initial_bytecode_offset, 0), None, is_jump_target, initial_bytecode_offset)

        else:
            oparg = ord(code[i]) + ord(code[i + 1]) * 256 + extended_arg

            extended_arg = 0
            i = i + 2
            if op == EXTENDED_ARG:
                extended_arg = oparg * 65536

            if op in hasconst:
                yield _Instruction(curr_op_name, op, _get_line(op_offset_to_line, initial_bytecode_offset, 0), co.co_consts[oparg], is_jump_target, initial_bytecode_offset)
            elif op in hasname:
                yield _Instruction(curr_op_name, op, _get_line(op_offset_to_line, initial_bytecode_offset, 0), co.co_names[oparg], is_jump_target, initial_bytecode_offset)
            elif op in hasjrel:
                argval = i + oparg
                yield _Instruction(curr_op_name, op, _get_line(op_offset_to_line, initial_bytecode_offset, 0), argval, is_jump_target, initial_bytecode_offset)
            elif op in haslocal:
                yield _Instruction(curr_op_name, op, _get_line(op_offset_to_line, initial_bytecode_offset, 0), co.co_varnames[oparg], is_jump_target, initial_bytecode_offset)
            elif op in hascompare:
                yield _Instruction(curr_op_name, op, _get_line(op_offset_to_line, initial_bytecode_offset, 0), cmp_op[oparg], is_jump_target, initial_bytecode_offset)
            elif op in hasfree:
                if free is None:
                    free = co.co_cellvars + co.co_freevars
                yield _Instruction(curr_op_name, op, _get_line(op_offset_to_line, initial_bytecode_offset, 0), free[oparg], is_jump_target, initial_bytecode_offset)
            else:
                yield _Instruction(curr_op_name, op, _get_line(op_offset_to_line, initial_bytecode_offset, 0), oparg, is_jump_target, initial_bytecode_offset)


def _iter_instructions(co):
    if sys.version_info[0] < 3:
        iter_in = _iter_as_bytecode_as_instructions_py2(co)
    else:
        iter_in = dis.Bytecode(co)
    iter_in = list(iter_in)

    bytecode_to_instruction = {}
    for instruction in iter_in:
        bytecode_to_instruction[instruction.offset] = instruction

    if iter_in:
        for instruction in iter_in:
            yield instruction


_return_op_names = {'RETURN_VALUE', 'RETURN_CONST'} if IS_PY312_OR_GREATER \
    else {'RETURN_VALUE'}


def collect_return_info(co, use_func_first_line=False):
    if not hasattr(co, 'co_lnotab'):
        return []

    if use_func_first_line:
        firstlineno = co.co_firstlineno
    else:
        firstlineno = 0

    lst = []
    op_offset_to_line = dict(dis.findlinestarts(co))
    for instruction in _iter_instructions(co):
        curr_op_name = instruction.opname
        if curr_op_name in _return_op_names:
            lst.append(ReturnInfo(_get_line(op_offset_to_line, instruction.offset, firstlineno, search=True)))

    return lst


def collect_try_except_info(co, use_func_first_line=False):
    if not hasattr(co, 'co_lnotab'):
        return []

    if use_func_first_line:
        firstlineno = co.co_firstlineno
    else:
        firstlineno = 0

    try_except_info_lst = []
    stack_in_setup = []

    if sys.version_info[0] < 3:
        iter_in = _iter_as_bytecode_as_instructions_py2(co)
    else:
        iter_in = dis.Bytecode(co)
    iter_in = list(iter_in)

    op_offset_to_line = dict(dis.findlinestarts(co))
    bytecode_to_instruction = {}
    for instruction in iter_in:
        bytecode_to_instruction[instruction.offset] = instruction

    if iter_in:
        for instruction in iter_in:
            curr_op_name = instruction.opname

            if curr_op_name in ('SETUP_EXCEPT', 'SETUP_FINALLY'):
                # We need to collect try..finally blocks too to make sure that
                # the stack_in_setup we're using to collect info is correct.
                # Note: On Py3.8 both except and finally statements use 'SETUP_FINALLY'.
                try_except_info = TryExceptInfo(
                    _get_line(op_offset_to_line, instruction.offset,
                              firstlineno, search=True),
                    is_finally=curr_op_name == 'SETUP_FINALLY'
                )
                try_except_info.except_bytecode_offset = instruction.argval
                try_except_info.except_line = _get_line(
                    op_offset_to_line,
                    try_except_info.except_bytecode_offset,
                    firstlineno,
                )

                stack_in_setup.append(try_except_info)

            elif curr_op_name == 'POP_EXCEPT':
                # On Python 3.8 there's no SETUP_EXCEPT (both except and finally start with SETUP_FINALLY),
                # so, we differentiate by a POP_EXCEPT.
                if IS_PY38_OR_GREATER:
                    stack_in_setup[-1].is_finally = False

            elif curr_op_name == 'RAISE_VARARGS':
                # We want to know about reraises and returns inside of except blocks (unfortunately
                # a raise appears to the debugger as a return, so, we may need to differentiate).
                if instruction.argval == 0:
                    for info in stack_in_setup:
                        info.raise_lines_in_except.append(
                            _get_line(op_offset_to_line, instruction.offset, firstlineno, search=True))

            elif curr_op_name == 'END_FINALLY':  # The except block also ends with 'END_FINALLY'.
                stack_in_setup[-1].except_end_bytecode_offset = instruction.offset
                stack_in_setup[-1].except_end_line = _get_line(op_offset_to_line, instruction.offset, firstlineno, search=True)
                if not stack_in_setup[-1].is_finally:
                    # Don't add try..finally blocks.
                    try_except_info_lst.append(stack_in_setup[-1])
                del stack_in_setup[-1]

        while stack_in_setup:
            # On Py3 the END_FINALLY may not be there (so, the end of the function is also the end
            # of the stack).
            stack_in_setup[-1].except_end_bytecode_offset = instruction.offset
            stack_in_setup[-1].except_end_line = _get_line(op_offset_to_line, instruction.offset, firstlineno, search=True)
            if not stack_in_setup[-1].is_finally:
                # Don't add try..finally blocks.
                try_except_info_lst.append(stack_in_setup[-1])
            del stack_in_setup[-1]

    return try_except_info_lst
