#!/usr/bin/env python3
# -*- encoding: Utf-8 -*-
from typing import List, Dict, Set, Sequence, Union, Optional, Iterator, Any
from logging import warning
from io import BytesIO

from hermes_dec.parsers.hbc_opcodes import (
    hbc51,
    hbc58,
    hbc59,
    hbc61,
    hbc62,
    hbc68,
    hbc69,
    hbc70,
    hbc72,
    hbc73,
    hbc76,
    hbc80,
    hbc81,
    hbc82,
    hbc83,
    hbc84,
    hbc85,
    hbc86,
    hbc87,
    hbc89,
    hbc90,
    hbc92,
    hbc95,
    hbc97,
    hbc98,
    hbc99,
)
from hermes_dec.parsers.serialized_literal_parser import (
    unpack_slp_array,
    SLPArray,
    SLPValue,
    TagType,
)
from hermes_dec.parsers.hbc_opcodes.def_classes import (
    OperandMeaning,
    Instruction,
)


class ParsedInstruction:
    inst: Instruction
    arg1: object
    arg2: object
    arg3: object
    arg4: object
    arg5: object
    arg6: object
    switch_jump_table: Optional[List[int]]
    original_pos: int
    next_pos: int
    hbc_reader: 'HBCReader'

    def __repr__(self):
        operands = [
            '%s: %s'
            % (
                (
                    self.inst.operands[index].operand_meaning.name
                    if self.inst.operands[index].operand_meaning
                    else self.inst.operands[index].operand_type.name
                ),
                getattr(self, 'arg%d' % (index + 1)),
            )
            for index in range(len(self.inst.operands))
        ]

        comment = ''
        for operand_index, operand in enumerate(self.inst.operands):
            if operand.operand_meaning:
                operand_value = getattr(self, 'arg%d' % (operand_index + 1))
                if operand.operand_meaning == OperandMeaning.string_id:
                    comment += '  # String: %r (%s)' % (
                        self.hbc_reader.strings[operand_value],
                        self.hbc_reader.string_kinds[operand_value].name,
                    )
                elif operand.operand_meaning == OperandMeaning.bigint_id:
                    comment += (
                        '  # BigInt: %s'
                        % (self.hbc_reader.bigint_values[operand_value])
                    )
                elif operand.operand_meaning == OperandMeaning.function_id:
                    function_header = self.hbc_reader.function_headers[
                        operand_value
                    ]
                    comment += (
                        '  # Function: [#%d %s of %d bytes]: %d params @ offset 0x%08x'
                        % (
                            operand_value,
                            self.hbc_reader.strings[
                                function_header.functionName
                            ],
                            function_header.bytecodeSizeInBytes,
                            function_header.paramCount,
                            function_header.offset,
                        )
                    )
            elif operand.operand_type.name in ('Addr8', 'Addr32'):
                operand_value = getattr(self, 'arg%d' % (operand_index + 1))
                comment += '  # Address: %08x' % (
                    self.original_pos + operand_value
                )
        if self.inst.name in ('NewArrayWithBuffer', 'NewArrayWithBufferLong'):
            comment += '  # Array: [%s]' % ', '.join(
                unpack_slp_array(
                    (
                        self.hbc_reader.arrays
                        if self.hbc_reader.header.version < 97
                        else self.hbc_reader.literal_values
                    )[self.arg4 :],
                    self.arg3,
                ).to_strings(self.hbc_reader.strings)
            )
        elif self.inst.name in (
            'NewObjectWithBuffer',
            'NewObjectWithBufferLong',
        ):
            if self.hbc_reader.header.version < 97:
                comment += '  # Object: {%s}' % ', '.join(
                    '%s: %s' % (key, value)
                    for key, value in zip(
                        unpack_slp_array(
                            self.hbc_reader.object_keys[self.arg4 :], self.arg3
                        ).to_strings(self.hbc_reader.strings),
                        unpack_slp_array(
                            (self.hbc_reader.object_values)[self.arg5 :],
                            self.arg3,
                        ).to_strings(self.hbc_reader.strings),
                    )
                )
            else:
                shape_keys = self.hbc_reader.object_shape_keys[self.arg2]
                comment += '  # Object: {%s}' % ', '.join(
                    '%s: %s' % (key, value)
                    for key, value in zip(
                        shape_keys,
                        unpack_slp_array(
                            (self.hbc_reader.literal_values)[self.arg3 :],
                            len(shape_keys),
                        ).to_strings(self.hbc_reader.strings),
                    )
                )
        elif self.inst.name in (
            'CallBuiltin',
            'CallBuiltinLong',
            'GetBuiltinClosure',
        ):
            builtin_number = self.arg2
            builtin_functions = get_builtin_functions(
                self.hbc_reader.parser_module
            )
            comment += '  # Built-in function: [#%d %s]' % (
                builtin_number,
                builtin_functions[builtin_number],
            )
        elif self.inst.name == 'SwitchImm':
            comment += '  # Jump table: [%s]' % ', '.join(
                '%08x' % value for value in self.switch_jump_table
            )

        return f'{"%08x" % self.original_pos}: <{self.inst.name}>: <{", ".join(operands)}>{comment}'


def get_parser(bytecode_version: int) -> 'module':
    parser_module_tbl = {
        51: hbc51,
        58: hbc58,
        59: hbc59,
        61: hbc61,
        62: hbc62,
        68: hbc68,
        69: hbc69,
        70: hbc70,
        # Before version 72/0.4.0, the bytecode format used predefined
        # string identifiers (which were removed in commit 63fb517)
        # These are not supported in the current code setup
        72: hbc72,
        73: hbc73,
        76: hbc76,
        80: hbc80,
        81: hbc81,
        82: hbc82,
        83: hbc83,
        84: hbc84,
        85: hbc85,
        86: hbc86,
        87: hbc87,
        89: hbc89,
        90: hbc90,
        92: hbc92,
        # The changes introduced in Hermes bytecode version 92
        # were reverted to the state of version 91 after a few
        # days from their introduction in the Git tree, into a
        # new version 93, and reintroduced in version 94.
        93: hbc90,
        94: hbc92,
        95: hbc95,
        # Bytecode version 96 has no opcode changes vs 95, only the
        # regex engine gains an INDICES flag (already handled).
        96: hbc95,
        # Bytecode version 97 marks the first updates in static Hermes
        # and is based off bytecode version 90
        97: hbc97,
        98: hbc98,
        99: hbc99,
    }

    if bytecode_version < 72:
        warning(
            'This file uses an ancient Hermes bytecode format, which '
            + 'is not supported.'
        )

    elif bytecode_version > 96:
        warning(
            (
                'Bytecode version %d corresponds to a development or '
                + 'recent version of the Hermes bytecode and is not '
                + 'formally supported by the current tool.'
            )
            % bytecode_version
        )

    for min_version in reversed(sorted(parser_module_tbl)):
        if bytecode_version >= min_version:
            parser_module = parser_module_tbl[min_version]
            break

    return parser_module


def get_builtin_functions(parser_module: 'module') -> List[str]:

    return parser_module._builtin_function_names


# Read and parse the actual bytecode (+ switch tables) for
# a given function header


def parse_hbc_bytecode(
    function_header: object, hbc_reader: 'HBCReader'
) -> Iterator[ParsedInstruction]:

    hbc_reader.file_buffer.seek(function_header.offset)

    buf = BytesIO(
        hbc_reader.file_buffer.read(function_header.bytecodeSizeInBytes)
    )

    while True:
        original_pos = buf.tell()

        opcode = buf.read(1)
        if not opcode:
            break
        opcode = opcode[0]

        inst = hbc_reader.parser_module._opcode_to_instruction.get(opcode)
        if not inst:
            raise NotImplementedError(
                'Opcode "0x%02x" is currently not implemented within hermes-dec.'
                % opcode
            )

        structure = inst.structure()
        buf.readinto(structure)

        result = ParsedInstruction()
        result.inst = inst
        result.original_pos = original_pos
        result.next_pos = original_pos + inst.binary_size
        result.hbc_reader = hbc_reader

        for operand in ('arg1', 'arg2', 'arg3', 'arg4', 'arg5', 'arg6'):
            if hasattr(structure, operand):
                setattr(result, operand, getattr(structure, operand))

        if inst.name == 'SwitchImm':
            result.switch_jump_table = []
            hbc_reader.file_buffer.seek(
                function_header.offset + original_pos + structure.arg2
            )
            hbc_reader.align_over_padding()

            for jump_table_entry in range(structure.arg4, structure.arg5 + 1):
                result.switch_jump_table.append(
                    int.from_bytes(hbc_reader.file_buffer.read(4), 'little')
                    + original_pos
                )

        yield result
