# Copyright lowRISC contributors.
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0

from typing import Dict, Optional, Sequence

import libcst as cst
from libcst._nodes.internal import CodegenState, visit_required


def make_aref(name: str, idx: cst.BaseExpression) -> cst.Subscript:
    sub_elt = cst.SubscriptElement(slice=cst.Index(value=idx))
    return cst.Subscript(value=cst.Name(name), slice=[sub_elt])


class NBAssignTarget(cst.AssignTarget):
    '''The target of a (delayed) state update'''
    def _codegen_impl(self, state: CodegenState) -> None:
        with state.record_syntactic_position(self):
            self.target._codegen(state)

        self.whitespace_before_equal._codegen(state)
        # U+21D0 is "Leftwards Double Arrow" (a nice unicode rendering of
        # SystemVerilog's "<=" which doesn't collide with less-than-or-equal.
        state.add_token("\u21d0")
        self.whitespace_after_equal._codegen(state)


class NBAssign(cst.BaseSmallStatement):
    '''An assignment statement that models a (delayed) state update'''

    def __init__(self, target: NBAssignTarget, value: cst.BaseExpression):
        super().__init__()
        self.target = target
        self.value = value

    def _visit_and_replace_children(self,
                                    visitor: cst.CSTVisitorT) -> "NBAssign":
        target = visit_required(self, "target", self.target, visitor)
        value = visit_required(self, "value", self.value, visitor)
        return NBAssign(target=target, value=value)

    def _codegen_impl(self,
                      state: CodegenState,
                      default_semicolon: bool = False) -> None:
        with state.record_syntactic_position(self):
            self.target._codegen(state)
            self.value._codegen(state)

    @staticmethod
    def make(lhs: cst.BaseAssignTargetExpression,
             rhs: cst.BaseExpression) -> 'NBAssign':
        return NBAssign(target=NBAssignTarget(target=lhs),
                        value=rhs)


class ImplTransformer(cst.CSTTransformer):
    '''An AST visitor used to extract documentation from the ISS'''
    def __init__(self) -> None:
        self.impls = {}  # type: Dict[str, Sequence[cst.BaseStatement]]

        self.cur_class = None  # type: Optional[str]

    def visit_ClassDef(self, node: cst.ClassDef) -> Optional[bool]:
        assert self.cur_class is None
        self.cur_class = node.name.value
        return None

    def leave_ClassDef(self,
                       orig: cst.ClassDef,
                       updated: cst.ClassDef) -> cst.BaseStatement:
        self.cur_class = None
        return updated

    def leave_Attribute(self,
                        orig: cst.Attribute,
                        updated: cst.Attribute) -> cst.BaseExpression:
        if isinstance(updated.value, cst.Name):
            stem = updated.value.value

            # Strip out "self." references. In the ISS code, a field in the
            # instruction appears as self.field_name. In the documentation, we
            # can treat all the instruction fields as being in scope.
            if stem == 'self':
                return updated.attr

            # Replace state.dmem with DMEM. This is an object in the ISS code,
            # so you see things like state.dmem.load_u32(...). We keep the
            # "object-orientated" style (so DMEM.load_u32(...)) because we need
            # to distinguish between 32-bit and 256-bit loads.
            if stem == 'state' and updated.attr.value == 'dmem':
                return cst.Name(value='DMEM')

        if isinstance(updated.value, cst.Attribute):
            # This attribute looks like A.B.C where B, C are names and A may be
            # a further attribute or it might be a name.
            attr_a = updated.value.value
            attr_b = updated.value.attr.value
            attr_c = updated.attr.value

            if isinstance(attr_a, cst.Name):
                stem = attr_a.value

                # Replace state.csrs.flags with FLAGs: the flag groups are
                # stored in the CSRs in the ISS and the implementation, but
                # logically exist somewhat separately, so we want named
                # reads/writes from them to look different.
                if (stem, attr_b, attr_c) == ('state', 'csrs', 'flags'):
                    return cst.Name(value='FLAGs')

        return updated

    def leave_FunctionDef(self,
                          orig: cst.FunctionDef,
                          updated: cst.FunctionDef) -> cst.BaseStatement:
        if ((self.cur_class is None or
             updated.name.value != 'execute' or
             self.cur_class in self.impls)):
            return updated

        # The body of a function definition is always an IndentedBlock. Strip
        # that out to get at the statements inside.
        assert isinstance(updated.body, cst.IndentedBlock)
        self.impls[self.cur_class] = updated.body.body
        return updated

    @staticmethod
    def match_get_reg(call: cst.BaseExpression) -> Optional[cst.Subscript]:
        '''Extract a RegRef from state.gprs.get_reg(foo)

        Returns None if this isn't a match.

        '''
        if not isinstance(call, cst.Call):
            return None

        # We expect a single argument (which we take as the index)
        if len(call.args) != 1:
            return None

        getreg_idx = call.args[0].value

        # All we need to do still is check that call.func is an
        # attribute representing state.gprs.get_reg or state.wdrs.get_reg.
        if ((not isinstance(call.func, cst.Attribute) or
             call.func.attr.value != 'get_reg')):
            return None

        state_dot_reg = call.func.value
        if not isinstance(state_dot_reg, cst.Attribute):
            return None

        # Finally, state_dot_reg should be either state.gprs or state.wdrs
        if not (isinstance(state_dot_reg.value, cst.Name) and
                state_dot_reg.value.value == 'state'):
            return None

        regfile_name = state_dot_reg.attr.value
        if regfile_name == 'gprs':
            regfile_uname = 'GPRs'
        elif regfile_name == 'wdrs':
            regfile_uname = 'WDRs'
        else:
            return None

        return make_aref(regfile_uname, getreg_idx)

    @staticmethod
    def _spot_reg_read(node: cst.Call) -> Optional[cst.BaseExpression]:
        # Detect
        #
        #    state.gprs.get_reg(FOO).read_unsigned()
        #    state.gprs.get_reg(FOO).read_signed()
        #
        # and replace with the expressions
        #
        #    GPRs[FOO]
        #    from_2s_complement(GPRs[FOO])
        #
        # respectively.

        # In either case, we expect node.func to be some long attribute
        # (representing state.gprs.get_reg(FOO).read_X). For unsigned or
        # signed, we can check that it is indeed an Attribute and that
        # node.args is empty (neither function takes arguments).
        if node.args or not isinstance(node.func, cst.Attribute):
            return None

        # Now, check whether we're calling one of the functions we're
        # interested in.
        if node.func.attr.value == 'read_signed':
            signed = True
        elif node.func.attr.value == 'read_unsigned':
            signed = False
        else:
            return None

        # Check that node.func.value really does represent something of the
        # form "state.gprs.get_reg(FOO)".
        ret = ImplTransformer.match_get_reg(node.func.value)
        if ret is None:
            return None

        if signed:
            # If this is a call to read_signed, we want to wrap the returned
            # value in a call to a fake sign decode function.
            return cst.Call(func=cst.Name('from_2s_complement'),
                            args=[cst.Arg(value=ret)])
        else:
            return ret

    @staticmethod
    def _spot_csr_read(node: cst.Call) -> Optional[cst.BaseExpression]:
        # Detect
        #
        #    state.read_csr(FOO)
        #
        # and replace it with the expression
        #
        #    CSRs[FOO]

        # Check we have exactly one argument
        if len(node.args) != 1:
            return None

        # Check this is state.read_csr
        if not (isinstance(node.func, cst.Attribute) and
                isinstance(node.func.value, cst.Name) and
                node.func.value.value == 'state' and
                node.func.attr.value == 'read_csr'):
            return None

        return make_aref('CSRs', node.args[0].value)

    @staticmethod
    def _spot_wsr_read_idx(node: cst.Call) -> Optional[cst.BaseExpression]:
        # Detect
        #
        #    state.wsrs.read_at_idx(FOO)
        #
        # and replace it with the expression
        #
        #    WSRs[FOO]

        # Check we have exactly one argument
        if len(node.args) != 1:
            return None

        # Check this is state.wsrs.read_at_idx
        if not (isinstance(node.func, cst.Attribute) and
                isinstance(node.func.value, cst.Attribute) and
                isinstance(node.func.value.value, cst.Name) and
                node.func.value.value.value == 'state' and
                node.func.value.attr.value == 'wsrs' and
                node.func.attr.value == 'read_at_idx'):
            return None

        return make_aref('WSRs', node.args[0].value)

    @staticmethod
    def _spot_wsr_read_name(node: cst.Call) -> Optional[cst.BaseExpression]:
        # Detect
        #
        #    state.wsrs.FOO.read_unsigned()
        #
        # and replace it with the expression
        #
        #    FOO

        # Check we have no arguments
        if len(node.args) != 0:
            return None

        # Check this is A.B.C.D for names A, B, C, D.
        if not (isinstance(node.func, cst.Attribute) and
                isinstance(node.func.value, cst.Attribute) and
                isinstance(node.func.value.value, cst.Attribute) and
                isinstance(node.func.value.value.value, cst.Name)):
            return None

        a_name = node.func.value.value.value
        b_name = node.func.value.value.attr
        c_name = node.func.value.attr
        d_name = node.func.attr

        if not (a_name.value == 'state' and
                b_name.value == 'wsrs' and
                d_name.value == 'read_unsigned'):
            return None

        return c_name

    def leave_Call(self,
                   orig: cst.Call,
                   updated: cst.Call) -> cst.BaseExpression:
        # Handle:
        #
        #    state.gprs.get_reg(FOO).read_unsigned()
        #    state.gprs.get_reg(FOO).read_signed()
        #
        reg_read = ImplTransformer._spot_reg_read(updated)
        if reg_read is not None:
            return reg_read

        csr_read = ImplTransformer._spot_csr_read(updated)
        if csr_read is not None:
            return csr_read

        wsr_read_idx = ImplTransformer._spot_wsr_read_idx(updated)
        if wsr_read_idx is not None:
            return wsr_read_idx

        wsr_read_name = ImplTransformer._spot_wsr_read_name(updated)
        if wsr_read_name is not None:
            return wsr_read_name

        return updated

    @staticmethod
    def _spot_reg_write(node: cst.Expr) -> Optional[NBAssign]:
        # Spot
        #
        #   state.gprs.get_reg(foo).write_unsigned(bar)
        #   state.gprs.get_reg(foo).write_signed(bar)
        #
        # and turn them into
        #
        #   GPRs[FOO] = bar
        #   GPRs[FOO] = to_2s_complement(bar)

        if not isinstance(node.value, cst.Call):
            return None

        call = node.value
        if len(call.args) != 1 or not isinstance(call.func, cst.Attribute):
            return None

        value = call.args[0].value

        if call.func.attr.value == 'write_unsigned':
            rhs = value
        elif call.func.attr.value == 'write_signed':
            rhs = cst.Call(func=cst.Name('to_2s_complement'),
                           args=[cst.Arg(value=value)])
        else:
            return None

        # We expect call.func.value to be match state.gprs.get_reg(foo).
        # Extract the array reference if we can.
        reg_ref = ImplTransformer.match_get_reg(call.func.value)
        if reg_ref is None:
            return None

        return NBAssign.make(reg_ref, rhs)

    @staticmethod
    def _spot_csr_write(node: cst.Expr) -> Optional[NBAssign]:
        # Spot
        #
        #   state.write_csr(csr, new_val)
        #
        # and turn it into
        #
        #   CSRs[csr] = new_val

        if not isinstance(node.value, cst.Call):
            return None

        call = node.value
        if len(call.args) != 2 or not isinstance(call.func, cst.Attribute):
            return None

        if not (isinstance(call.func.value, cst.Name) and
                call.func.value.value == 'state' and
                call.func.attr.value == 'write_csr'):
            return None

        idx = call.args[0].value
        rhs = call.args[1].value

        return NBAssign.make(make_aref('CSRs', idx), rhs)

    @staticmethod
    def _spot_wsr_write_idx(node: cst.Expr) -> Optional[NBAssign]:
        # Spot
        #
        #   state.wsrs.write_at_idx(wsr, new_val)
        #
        # and turn it into
        #
        #   WSRs[wsr] = new_val

        if not isinstance(node.value, cst.Call):
            return None

        call = node.value
        if len(call.args) != 2 or not isinstance(call.func, cst.Attribute):
            return None

        func = call.func

        if not (isinstance(func.value, cst.Attribute) and
                isinstance(func.value.value, cst.Name) and
                func.value.value.value == 'state' and
                func.value.attr.value == 'wsrs' and
                func.attr.value == 'write_at_idx'):
            return None

        idx = call.args[0].value
        rhs = call.args[1].value

        return NBAssign.make(make_aref('WSRs', idx), rhs)

    @staticmethod
    def _spot_wsr_write_name(node: cst.Expr) -> Optional[NBAssign]:
        # Spot
        #
        #   state.wsrs.FOO.write_unsigned(new_val)
        #
        # and turn it into
        #
        #   FOO = new_val

        if not isinstance(node.value, cst.Call):
            return None

        call = node.value
        if len(call.args) != 1 or not isinstance(call.func, cst.Attribute):
            return None

        # Check this is A.B.C.D for names A, B, C, D.
        if not (isinstance(call.func, cst.Attribute) and
                isinstance(call.func.value, cst.Attribute) and
                isinstance(call.func.value.value, cst.Attribute) and
                isinstance(call.func.value.value.value, cst.Name)):
            return None

        a_name = call.func.value.value.value
        b_name = call.func.value.value.attr
        c_name = call.func.value.attr
        d_name = call.func.attr

        if not (a_name.value == 'state' and
                b_name.value == 'wsrs' and
                d_name.value == 'write_unsigned'):
            return None

        rhs = call.args[0].value

        return NBAssign.make(c_name, rhs)

    @staticmethod
    def _spot_flag_write(node: cst.Expr) -> Optional[NBAssign]:
        # Spot
        #
        #   state.set_flags(fg, flags)
        #
        # and turn it into
        #
        #   FLAGs[fg] = flags

        if not isinstance(node.value, cst.Call):
            return None

        call = node.value
        if len(call.args) != 2 or not isinstance(call.func, cst.Attribute):
            return None

        if not (isinstance(call.func.value, cst.Name) and
                call.func.value.value == 'state' and
                call.func.attr.value == 'set_flags'):
            return None

        fg = call.args[0].value
        flags = call.args[1].value

        return NBAssign.make(make_aref('FLAGs', fg), flags)

    @staticmethod
    def _spot_set_next_pc(node: cst.Expr) -> Optional[NBAssign]:
        # Spot
        #
        #   state.set_next_pc(next_pc)
        #
        # and turn it into
        #
        #   PC <= next_pc

        if not isinstance(node.value, cst.Call):
            return None

        call = node.value
        if len(call.args) != 1 or not isinstance(call.func, cst.Attribute):
            return None

        if not (isinstance(call.func.value, cst.Name) and
                call.func.value.value == 'state' and
                call.func.attr.value == 'set_next_pc'):
            return None

        next_pc = call.args[0].value

        return NBAssign.make(cst.Name(value='PC'), next_pc)

    def leave_Expr(self,
                   orig: cst.Expr,
                   updated: cst.Expr) -> cst.BaseSmallStatement:
        reg_write = ImplTransformer._spot_reg_write(updated)
        if reg_write is not None:
            return reg_write

        csr_write = ImplTransformer._spot_csr_write(updated)
        if csr_write is not None:
            return csr_write

        wsr_write_idx = ImplTransformer._spot_wsr_write_idx(updated)
        if wsr_write_idx is not None:
            return wsr_write_idx

        wsr_write_name = ImplTransformer._spot_wsr_write_name(updated)
        if wsr_write_name is not None:
            return wsr_write_name

        flag_write = ImplTransformer._spot_flag_write(updated)
        if flag_write is not None:
            return flag_write

        set_pc_next = ImplTransformer._spot_set_next_pc(updated)
        if set_pc_next is not None:
            return set_pc_next

        return updated


def read_implementation(path: str) -> Dict[str, str]:
    '''Read the implementation at path (probably insn.py)

    Returns a dictionary from instruction class name to its pseudo-code
    implementation. An instruction class name looks like ADDI (for addi) or
    BNADDM (for bn.addm).

    '''
    with open(path, 'r') as handle:
        node = cst.parse_module(handle.read())

    # Extract the function bodies
    visitor = ImplTransformer()
    node.visit(visitor)

    # Render the function bodies
    return {cls: ''.join(node.code_for_node(stmt) for stmt in body)
            for cls, body in visitor.impls.items()}
