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

'''Code representing an IP block for reggen'''

from typing import Dict, List, Optional, Sequence, Set, Tuple

import hjson  # type: ignore

from reggen.alert import Alert
from reggen.bus_interfaces import BusInterfaces
from reggen.clocking import Clocking, ClockingItem
from reggen.interrupt import Interrupt
from reggen.inter_signal import InterSignal
from reggen.lib import (check_keys, check_name, check_int, check_bool, check_list)
from reggen.params import ReggenParams, LocalParam
from reggen.reg_block import RegBlock
from reggen.signal import Signal
from reggen.countermeasure import CounterMeasure


REQUIRED_ALIAS_FIELDS = {
    'alias_impl': [
        's',
        "identifier for this alias implementation"
    ],
    'alias_target': [
        's',
        "name of the component to apply the alias file to"
    ],
    'registers': [
        'l',
        "list of alias register definition groups"
    ],
    'bus_interfaces': [
        'l',
        "bus interfaces for the device"
    ],
}

# TODO: we may want to support for countermeasure and parameter aliases
# in the future.
OPTIONAL_ALIAS_FIELDS: Dict[str, List[str]] = {
}

REQUIRED_FIELDS = {
    'name': ['s', "name of the component"],
    'clocking': ['l', "clocking for the device"],
    'bus_interfaces': ['l', "bus interfaces for the device"],
    'registers': [
        'l',
        "list of register definition groups and "
        "offset control groups"
    ]
}

OPTIONAL_FIELDS = {
    'human_name': ['s', "human-readable name of the component"],
    'one_line_desc': ['s', "one-line description of the component"],
    'one_paragraph_desc': ['s', "one-paragraph description of the component"],
    # Note: this revision list may be deprecated in the future.
    'revisions': ['l', "list with revision records"],
    'design_spec':
    ['s', "path to the design specification, relative to repo root"],
    'dv_doc': ['s', "path to the DV document, relative to repo root"],
    'hw_checklist': ['s', "path to the hw_checklist, relative to repo root"],
    'sw_checklist': ['s', "path to the sw_checklist, relative to repo root"],
    'design_stage': ['s', "design stage of module"],
    'dif_stage': ['s', 'DIF stage of module'],
    'verification_stage': ['s', "verification stage of module"],
    'notes': ['s', "random notes"],
    'version': ['s', "module version"],
    'life_stage': ['s', "life stage of module"],
    'commit_id': ['s', "commit ID of last stage sign-off"],
    'alert_list': ['lnw', "list of peripheral alerts"],
    'available_inout_list': ['lnw', "list of available peripheral inouts"],
    'available_input_list': ['lnw', "list of available peripheral inputs"],
    'available_output_list': ['lnw', "list of available peripheral outputs"],
    'expose_reg_if': ['pb', 'if set, expose reg interface in reg2hw signal'],
    'interrupt_list': ['lnw', "list of peripheral interrupts"],
    'inter_signal_list': ['l', "list of inter-module signals"],
    'no_auto_alert_regs': [
        's', "Set to true to suppress automatic "
        "generation of alert test registers. "
        "Defaults to true if no alert_list is present. "
        "Otherwise this defaults to false."
    ],
    'no_auto_intr_regs': [
        's', "Set to true to suppress automatic "
        "generation of interrupt registers. "
        "Defaults to true if no interrupt_list is present. "
        "Otherwise this defaults to false."
    ],
    'param_list': ['lp', "list of parameters of the IP"],
    'regwidth': ['d', "width of registers in bits (default 32)"],
    'reset_request_list': ['l', 'list of signals requesting reset'],
    'scan': ['pb', 'Indicates the module have `scanmode_i`'],
    'scan_reset': ['pb', 'Indicates the module have `scan_rst_ni`'],
    'scan_en': ['pb', 'Indicates the module has `scan_en_i`'],
    'SPDX-License-Identifier': [
        's', "License ientifier (if using pure json) "
        "Only use this if unable to put this "
        "information in a comment at the top of the "
        "file."
    ],
    'wakeup_list': ['lnw', "list of peripheral wakeups"],
    'countermeasures': ["ln", "list of countermeasures in this block"]
}


class IpBlock:
    def __init__(self,
                 name: str,
                 regwidth: int,
                 params: ReggenParams,
                 reg_blocks: Dict[Optional[str], RegBlock],
                 alias_impl: Optional[str],
                 interrupts: Sequence[Interrupt],
                 no_auto_intr: bool,
                 alerts: List[Alert],
                 no_auto_alert: bool,
                 scan: bool,
                 inter_signals: List[InterSignal],
                 bus_interfaces: BusInterfaces,
                 clocking: Clocking,
                 xputs: Tuple[Sequence[Signal],
                              Sequence[Signal],
                              Sequence[Signal]],
                 wakeups: Sequence[Signal],
                 reset_requests: Sequence[Signal],
                 expose_reg_if: bool,
                 scan_reset: bool,
                 scan_en: bool,
                 countermeasures: List[CounterMeasure]):
        assert reg_blocks

        # Check that register blocks are in bijection with device interfaces
        reg_block_names = reg_blocks.keys()
        dev_if_names = []  # type: List[Optional[str]]
        dev_if_names += bus_interfaces.named_devices
        if bus_interfaces.has_unnamed_device:
            dev_if_names.append(None)
        assert set(reg_block_names) == set(dev_if_names)

        self.name = name
        self.regwidth = regwidth
        self.reg_blocks = reg_blocks
        self.alias_impl = alias_impl
        self.params = params
        self.interrupts = interrupts
        self.no_auto_intr = no_auto_intr
        self.alerts = alerts
        self.no_auto_alert = no_auto_alert
        self.scan = scan
        self.inter_signals = inter_signals
        self.bus_interfaces = bus_interfaces
        self.clocking = clocking
        self.xputs = xputs
        self.wakeups = wakeups
        self.reset_requests = reset_requests
        self.expose_reg_if = expose_reg_if
        self.scan_reset = scan_reset
        self.scan_en = scan_en
        self.countermeasures = countermeasures

    @staticmethod
    def from_raw(param_defaults: List[Tuple[str, str]],
                 raw: object,
                 where: str) -> 'IpBlock':

        rd = check_keys(raw, 'block at ' + where,
                        list(REQUIRED_FIELDS.keys()),
                        list(OPTIONAL_FIELDS.keys()))

        name = check_name(rd['name'], 'name of block at ' + where)

        what = '{} block at {}'.format(name, where)

        r_regwidth = rd.get('regwidth')
        if r_regwidth is None:
            regwidth = 32
        else:
            regwidth = check_int(r_regwidth, 'regwidth field of ' + what)
            if regwidth <= 0:
                raise ValueError('Invalid regwidth field for {}: '
                                 '{} is not positive.'
                                 .format(what, regwidth))

        params = ReggenParams.from_raw('parameter list for ' + what,
                                       rd.get('param_list', []))
        try:
            params.apply_defaults(param_defaults)
        except (ValueError, KeyError) as err:
            raise ValueError('Failed to apply defaults to params: {}'
                             .format(err)) from None

        init_block = RegBlock(regwidth, params)

        interrupts = Interrupt.from_raw_list('interrupt_list for block {}'
                                             .format(name),
                                             rd.get('interrupt_list', []))
        alerts = Alert.from_raw_list('alert_list for block {}'
                                     .format(name),
                                     rd.get('alert_list', []))
        known_cms = {}
        raw_cms = rd.get('countermeasures', [])

        countermeasures = CounterMeasure.from_raw_list(
            'countermeasure list for block {}'
            .format(name), raw_cms)

        # Ensure that the countermeasures are unique
        for x in countermeasures:
            if str(x) in known_cms:
                raise RuntimeError(f"Duplicate countermeasure {str(x)}")
            else:
                known_cms.update({str(x): 1})

        no_auto_intr = check_bool(rd.get('no_auto_intr_regs', not interrupts),
                                  'no_auto_intr_regs field of ' + what)

        no_auto_alert = check_bool(rd.get('no_auto_alert_regs', not alerts),
                                   'no_auto_alert_regs field of ' + what)

        if interrupts and not no_auto_intr:
            if interrupts[-1].bits.msb >= regwidth:
                raise ValueError("Interrupt list for {} is too wide: "
                                 "msb is {}, which doesn't fit with a "
                                 "regwidth of {}."
                                 .format(what,
                                         interrupts[-1].bits.msb, regwidth))
            init_block.make_intr_regs(interrupts)

        if alerts:
            if not no_auto_alert:
                if len(alerts) > regwidth:
                    raise ValueError("Interrupt list for {} is too wide: "
                                     "{} alerts don't fit with a regwidth of {}."
                                     .format(what, len(alerts), regwidth))
                init_block.make_alert_regs(alerts)

            # Generate a NumAlerts parameter
            existing_param = params.get('NumAlerts')
            if existing_param is not None:
                if ((not isinstance(existing_param, LocalParam) or
                     existing_param.param_type != 'int' or
                     existing_param.value != str(len(alerts)))):
                    raise ValueError('Conflicting definition of NumAlerts '
                                     'parameter.')
            else:
                params.add(LocalParam(name='NumAlerts',
                                      desc='Number of alerts',
                                      param_type='int',
                                      value=str(len(alerts))))

        scan = check_bool(rd.get('scan', False), 'scan field of ' + what)

        r_inter_signals = check_list(rd.get('inter_signal_list', []),
                                     'inter_signal_list field')
        inter_signals = [
            InterSignal.from_raw('entry {} of the inter_signal_list field'
                                 .format(idx + 1),
                                 entry)
            for idx, entry in enumerate(r_inter_signals)
        ]

        bus_interfaces = (BusInterfaces.
                          from_raw(rd['bus_interfaces'],
                                   'bus_interfaces field of ' + where))
        inter_signals += bus_interfaces.inter_signals()

        clocking = Clocking.from_raw(rd['clocking'],
                                     'clocking field of ' + what)

        reg_blocks = RegBlock.build_blocks(init_block, rd['registers'],
                                           bus_interfaces,
                                           clocking, False)

        xputs = (
            Signal.from_raw_list('available_inout_list for block ' + name,
                                 rd.get('available_inout_list', [])),
            Signal.from_raw_list('available_input_list for block ' + name,
                                 rd.get('available_input_list', [])),
            Signal.from_raw_list('available_output_list for block ' + name,
                                 rd.get('available_output_list', []))
        )
        wakeups = Signal.from_raw_list('wakeup_list for block ' + name,
                                       rd.get('wakeup_list', []))
        rst_reqs = Signal.from_raw_list('reset_request_list for block ' + name,
                                        rd.get('reset_request_list', []))

        expose_reg_if = check_bool(rd.get('expose_reg_if', False),
                                   'expose_reg_if field of ' + what)

        scan_reset = check_bool(rd.get('scan_reset', False),
                                'scan_reset field of ' + what)

        scan_en = check_bool(rd.get('scan_en', False),
                             'scan_en field of ' + what)

        # Check that register blocks are in bijection with device interfaces
        reg_block_names = reg_blocks.keys()
        dev_if_names = []  # type: List[Optional[str]]
        dev_if_names += bus_interfaces.named_devices
        if bus_interfaces.has_unnamed_device:
            dev_if_names.append(None)
        if set(reg_block_names) != set(dev_if_names):
            raise ValueError("IP block {} defines device interfaces, named {} "
                             "but its registers don't match (they are keyed "
                             "by {})."
                             .format(name, dev_if_names,
                                     list(reg_block_names)))

        return IpBlock(name, regwidth, params, reg_blocks, None,
                       interrupts, no_auto_intr, alerts, no_auto_alert,
                       scan, inter_signals, bus_interfaces, clocking, xputs,
                       wakeups, rst_reqs, expose_reg_if, scan_reset, scan_en,
                       countermeasures)

    @staticmethod
    def from_text(txt: str,
                  param_defaults: List[Tuple[str, str]],
                  where: str) -> 'IpBlock':
        '''Load an IpBlock from an hjson description in txt'''
        return IpBlock.from_raw(param_defaults,
                                hjson.loads(txt, use_decimal=True),
                                where)

    @staticmethod
    def from_path(path: str,
                  param_defaults: List[Tuple[str, str]]) -> 'IpBlock':
        '''Load an IpBlock from an hjson description in a file at path'''
        with open(path, 'r', encoding='utf-8') as handle:
            return IpBlock.from_text(handle.read(), param_defaults,
                                     'file at {!r}'.format(path))

    def alias_from_raw(self,
                       scrub: bool,
                       raw: object,
                       where: str) -> None:
        '''Parses and validates an alias reg block and adds it to this IpBlock.

        The alias register definitions are compared with the corresponding
        generic register definitions in self.reg_blocks to ensure that the
        register and field structure is the same. Only a subset of register
        and field attributes may differ and all other attributes must be
        identical. The overridable attributes are defined in register.py and
        field.py, but typically comprise attributes like 'name', 'desc',
        'resval' and 'tags'.

        The alias register information is then applied to the self.reg_blocks
        datastructure. Generic register descriptions with no associated alias
        register definition just remain unmodified, meaning that the user can
        choose to only provide alias overrides for a subset of all registers.
        The resulting "augmented" register block is therefore always guaranteed
        to be structurally identical to the unmodified generic register block.

        Note that the alias register definition also overrides the hier_path
        variable associated with the corresponding bus interfaces.

        Setting the scrub argument to True will scrub sensitive fields in the
        alias definition and replace the entire register block of the target
        interface with the scrubbed alias reg block. This is helpful to create
        the generic CSR structure matching the alias definition automatically.
        '''
        rd = check_keys(raw, 'block at ' + where,
                        list(REQUIRED_ALIAS_FIELDS.keys()),
                        list(OPTIONAL_ALIAS_FIELDS.keys()))

        alias_bus_interfaces = (BusInterfaces.
                                from_raw(rd['bus_interfaces'],
                                         'bus_interfaces of block at ' + where))
        if ((alias_bus_interfaces.has_unnamed_host or
             alias_bus_interfaces.named_hosts)):
            raise ValueError("Alias registers cannot be defined for host "
                             "interfaces (in block at {})."
                             .format(where))
        # Alias register definitions are only compatible with named devices.
        if ((alias_bus_interfaces.has_unnamed_device or
             self.bus_interfaces.has_unnamed_device)):
            raise ValueError("Alias registers must use named devices "
                             "(in block at {}).".format(where))

        # Check that the device interface names are
        # a subset of the already defined register blocks
        bus_device_names = set(self.bus_interfaces.named_devices)
        alias_bus_device_names = set(alias_bus_interfaces.named_devices)
        if not alias_bus_device_names.issubset(bus_device_names):
            raise ValueError("Alias file {} refers to device names {} that "
                             "do not map to device names in {}."
                             .format(where, list(alias_bus_device_names),
                                     self.name))

        self.alias_impl = check_name(rd['alias_impl'],
                                     'alias_impl of block at ' + where)

        alias_target = check_name(rd['alias_target'],
                                  'alias_target of block at ' + where)

        if alias_target != self.name:
            raise ValueError("Alias target block name {} in {} "
                             "does not match block name {}."
                             .format(alias_target, where, self.name))

        init_block = RegBlock(self.regwidth, self.params)

        alias_reg_blocks = RegBlock.build_blocks(init_block, rd['registers'],
                                                 self.bus_interfaces,
                                                 self.clocking,
                                                 True)

        # Check that alias register block names are
        # a subset of the already defined register blocks
        alias_reg_block_names = set(alias_reg_blocks.keys())

        if not alias_reg_block_names.issubset(set(self.reg_blocks.keys())):
            raise ValueError("Alias file {} refers to register blocks {} that "
                             "do not map to register blocks in {}."
                             .format(where, list(alias_reg_block_names),
                                     self.name))

        # Check that the alias bus interface names and register blocks match
        if alias_reg_block_names != alias_bus_device_names:
            raise ValueError("Interface and register block names do not match "
                             "in {}.".format(where))

        # Validate alias registers against the generic reg blocks,
        # and enhance the information in the existing datastructures.
        for block_key, alias_block in alias_reg_blocks.items():
            # Double check the interface definition options
            if self.bus_interfaces.device_async:
                if not alias_bus_interfaces.device_async:
                    raise ValueError('Missing device_async key in alias '
                                     'interface {} in {}'
                                     .format(block_key, where))
                if ((alias_bus_interfaces.device_async[block_key] !=
                     self.bus_interfaces.device_async[block_key])):
                    raise ValueError('Inconsistent configuration of interface '
                                     '{} in {}'
                                     .format(block_key, where))

            if scrub:
                # scrub alias definitions and replace the entire block.
                alias_block.scrub_alias(where)
                self.reg_blocks[block_key] = alias_block
            else:
                # Override hier path of aliased interface
                hier_path = alias_bus_interfaces.device_hier_paths[block_key]
                self.bus_interfaces.device_hier_paths[block_key] = hier_path

                # Validate and apply alias definitions
                self.reg_blocks[block_key].apply_alias(alias_block, where)

    def alias_from_text(self,
                        scrub: bool,
                        txt: str,
                        where: str) -> None:
        '''Load alias regblocks from an hjson description in txt'''
        self.alias_from_raw(scrub, hjson.loads(txt, use_decimal=True), where)

    def alias_from_path(self,
                        scrub: bool,
                        path: str) -> None:
        '''Load alias regblocks from an hjson description in a file at path'''
        with open(path, 'r', encoding='utf-8') as handle:
            self.alias_from_text(scrub,
                                 handle.read(),
                                 'alias file at {!r}'.format(path))

    def _asdict(self) -> Dict[str, object]:
        ret = {
            'name': self.name,
            'regwidth': self.regwidth
        }
        if len(self.reg_blocks) == 1 and None in self.reg_blocks:
            ret['registers'] = self.reg_blocks[None].as_dicts()
        else:
            ret['registers'] = {k: v.as_dicts()
                                for k, v in self.reg_blocks.items()}

        ret['param_list'] = self.params.as_dicts()
        ret['interrupt_list'] = self.interrupts
        ret['no_auto_intr_regs'] = self.no_auto_intr
        ret['alert_list'] = self.alerts
        ret['no_auto_alert_regs'] = self.no_auto_alert
        ret['scan'] = self.scan
        ret['inter_signal_list'] = self.inter_signals
        ret['bus_interfaces'] = self.bus_interfaces.as_dicts()

        ret['clocking'] = self.clocking.items

        inouts, inputs, outputs = self.xputs
        if inouts:
            ret['available_inout_list'] = inouts
        if inputs:
            ret['available_input_list'] = inputs
        if outputs:
            ret['available_output_list'] = outputs

        if self.wakeups:
            ret['wakeup_list'] = self.wakeups
        if self.reset_requests:
            ret['reset_request_list'] = self.reset_requests

        ret['scan_reset'] = self.scan_reset
        ret['scan_en'] = self.scan_en

        return ret

    def get_rnames(self) -> Set[str]:
        ret = set()  # type: Set[str]
        for rb in self.reg_blocks.values():
            ret = ret.union(set(rb.name_to_offset.keys()))
        return ret

    def get_signals_as_list_of_dicts(self) -> List[Dict[str, object]]:
        '''Look up and return signal by name'''
        result = []
        for iodir, xput in zip(('inout', 'input', 'output'), self.xputs):
            for sig in xput:
                result.append(sig.as_nwt_dict(iodir))
        return result

    def get_signal_by_name_as_dict(self, name: str) -> Dict[str, object]:
        '''Look up and return signal by name'''
        sig_list = self.get_signals_as_list_of_dicts()
        for sig in sig_list:
            if sig['name'] == name:
                return sig
        else:
            raise ValueError("Signal {} does not exist in IP block {}"
                             .format(name, self.name))

    def has_shadowed_reg(self) -> bool:
        '''Return boolean indication whether reg block contains shadowed registers'''

        for rb in self.reg_blocks.values():
            if rb.has_shadowed_reg():
                return True

        # if we are here, then no one has a shadowed register
        return False

    def get_primary_clock(self) -> ClockingItem:
        '''Return primary clock of an block'''

        return self.clocking.primary

    def check_cm_annotations(self,
                             rtl_names: Dict[str, List[Tuple[str, int]]],
                             where: str) -> None:
        '''Check RTL annotations against countermeasure list of this block'''

        what = '{} block at {}'.format(self.name, where)
        CounterMeasure.check_annotation_list(what,
                                             rtl_names,
                                             self.countermeasures)
