# Copyright lowRISC contributors.
# Licensed under the Apache License, Version 2.0, see LICENSE for details.
# SPDX-License-Identifier: Apache-2.0
"""
Generate Rust constants from validated register JSON tree
"""

import io
import logging as log
import sys
import textwrap
import warnings
from typing import Optional, Set, TextIO


from reggen.field import Field
from reggen.ip_block import IpBlock
from reggen.params import LocalParam
from reggen.register import Register
from reggen.multi_register import MultiRegister
from reggen.signal import Signal
from reggen.window import Window


def genout(outfile: TextIO, msg: str) -> None:
    outfile.write(msg)


def to_snake_case(s: str) -> str:
    val = []
    for i, ch in enumerate(s):
        if i > 0 and ch.isupper():
            val.append('_')
        val.append(ch)
    return ''.join(val)


def as_define(s: str) -> str:
    s = s.upper()
    r = ''
    for i in range(0, len(s)):
        r += s[i] if s[i].isalnum() else '_'
    return r


def first_line(s: str) -> str:
    """Returns the first line of a multi-line string"""
    return s.splitlines()[0]


def format_comment(s: str) -> str:
    """Formats a string to comment wrapped to an 80 character line width

    Returns wrapped string including newline and // comment characters.
    """
    comment = textwrap.wrap(s,
                            width=77,
                            initial_indent='// ',
                            subsequent_indent='// ')
    return '\n'.join(comment) + '\n'


def data_type(name: str, val: int, as_hex: bool) -> str:
    """ Returns proper data type for name-value pair. """

    if name.endswith("_OFFSET") or name.endswith("_BASE_ADDR"):
        return "usize"

    if val.bit_length() > 32:
        log.error(name + " value exceeds 32 bit " + str(val))
        sys.exit(1)

    if not as_hex and val < 0:
        return "i32"

    return "u32"


def gen_const(outstr: TextIO,
              name: str,
              suffix: str,
              val: int,
              existing_defines: Set[str],
              as_hex: bool = False) -> str:
    r"""Produces a pub const string. Result includes newline.

    Arguments:
    name - Name of the constant
    val - Value of the constant
    existing_defines - set of already generated define names.
        Error if `name` is in `existing_defines`.

    Example result:
    name = 'A_NAME'
    val = '10'

    pub const A_NAME: u32 = 10

    """

    suffix = '' if not suffix.strip() else '_' + suffix
    name = name + suffix
    if name in existing_defines:
        log.error("Duplicate pub const for " + name)
        sys.exit(1)

    define_declare = 'pub const ' + name + ': ' + data_type(name, val, as_hex)

    val_str = hex(val) if as_hex else str(val)
    oneline_define = define_declare + ' = ' + val_str + ';'

    existing_defines.add(name)

    output = oneline_define + '\n'
    genout(outstr, output)
    return output


def gen_const_register(outstr: TextIO,
                       reg: Register,
                       comp: str,
                       width: int,
                       rnames: Set[str],
                       existing_defines: Set[str]) -> None:
    rname = reg.name
    offset = reg.offset

    genout(outstr, format_comment(first_line(reg.desc)))
    defname = as_define(comp + '_' + rname)
    gen_const(outstr, defname, 'REG_OFFSET', offset, existing_defines, True)

    for field in reg.fields:
        dname = defname + '_' + as_define(field.name)
        field_width = field.bits.width()

        if field_width == 1:
            # single bit
            gen_const(outstr, dname, 'BIT', field.bits.lsb, existing_defines)
        else:
            # multiple bits (unless it is the whole register)
            if field_width != width:
                mask = field.bits.bitmask() >> field.bits.lsb
                gen_const(outstr, dname, 'MASK', mask, existing_defines, True)
                gen_const(outstr, dname, 'OFFSET', field.bits.lsb, existing_defines)

            if field.enum is not None:
                for enum in field.enum:
                    ename = as_define(enum.name)
                    gen_const(
                        outstr,
                        defname + '_' + as_define(field.name),
                        'VALUE_' + ename,
                        enum.value,
                        existing_defines,
                        True)

    genout(outstr, '\n')
    return


def gen_const_window(outstr: TextIO,
                     win: Window,
                     comp: str,
                     regwidth: int,
                     rnames: Set[str],
                     existing_defines: Set[str]) -> None:
    offset = win.offset

    genout(outstr, format_comment('Memory area: ' + first_line(win.desc)))
    defname = as_define(comp + '_' + win.name)
    gen_const(outstr, defname, 'REG_OFFSET', offset, existing_defines, True)
    items = win.items
    gen_const(outstr, defname, 'SIZE_WORDS', items, existing_defines)
    items = items * (regwidth // 8)
    gen_const(outstr, defname, 'SIZE_BYTES', items, existing_defines)

    wid = win.validbits
    if (wid != regwidth):
        mask = (1 << wid) - 1
        gen_const(outstr, defname, 'MASK', mask, existing_defines, True)


def gen_rust_module_param(outstr: TextIO,
                          param: LocalParam,
                          module_name: str,
                          existing_defines: Set[str]) -> None:
    # Presently there is only one type (int), however if the new types are
    # added, they potentially need to be handled differently.
    known_types = ["int"]
    if param.param_type not in known_types:
        warnings.warn("Cannot generate a module define of type {}"
                      .format(param.param_type))
        return

    if param.desc is not None:
        genout(outstr, format_comment(first_line(param.desc)))
    # Heuristic: if the name already has underscores, it's already snake_case,
    # otherwise, assume StudlyCaps and covert it to snake_case.
    param_name = param.name if '_' in param.name else to_snake_case(param.name)
    define_name = as_define(module_name + '_PARAM_' + param_name)
    if param.param_type == "int":
        gen_const(outstr, define_name, '', int(param.value), existing_defines)

    genout(outstr, '\n')


def gen_const_module_params(outstr: TextIO,
                            module_data: IpBlock,
                            module_name: str,
                            register_width: int,
                            existing_defines: Set[str]) -> None:
    for param in module_data.params.get_localparams():
        gen_rust_module_param(outstr, param, module_name, existing_defines)

    genout(outstr, format_comment(first_line("Register width")))
    define_name = as_define(module_name + '_PARAM_REG_WIDTH')
    gen_const(outstr, define_name, '', register_width, existing_defines)
    genout(outstr, '\n')


def gen_multireg_field_defines(outstr: TextIO,
                               regname: str,
                               field: Field,
                               subreg_num: int,
                               regwidth: int,
                               existing_defines: Set[str]) -> None:
    field_width = field.bits.width()
    fields_per_reg = regwidth // field_width

    suffix = as_define(field.name + "_FIELD_WIDTH")
    gen_const(outstr, regname, suffix, field_width, existing_defines)

    suffix = as_define(field.name + "_FIELDS_PER_REG")
    gen_const(outstr, regname, suffix, fields_per_reg, existing_defines)

    gen_const(outstr, regname, "MULTIREG_COUNT", subreg_num, existing_defines)

    genout(outstr, '\n')


def gen_const_multireg(outstr: TextIO,
                       multireg: MultiRegister,
                       component: str,
                       regwidth: int,
                       rnames: Set[str],
                       existing_defines: Set[str]) -> None:
    comment = multireg.reg.desc + " (common parameters)"
    genout(outstr, format_comment(first_line(comment)))
    if len(multireg.reg.fields) == 1:
        regname = as_define(component + '_' + multireg.reg.name)
        gen_multireg_field_defines(outstr, regname, multireg.reg.fields[0],
                                   len(multireg.regs), regwidth, existing_defines)
    else:
        log.warn("Non-homogeneous multireg " + multireg.reg.name +
                 " skip multireg specific data generation.")

    for subreg in multireg.regs:
        gen_const_register(outstr, subreg, component, regwidth, rnames,
                           existing_defines)


def gen_interrupt_field(outstr: TextIO,
                        interrupt: Signal,
                        component: str,
                        regwidth: int,
                        existing_defines: Set[str]) -> None:
    fieldlsb = interrupt.bits.lsb
    iname = interrupt.name
    defname = as_define(component + '_INTR_COMMON_' + iname)

    if interrupt.bits.width() == 1:
        # single bit
        gen_const(outstr, defname, 'BIT', fieldlsb, existing_defines)
    else:
        # multiple bits (unless it is the whole register)
        if interrupt.bits.width() != regwidth:
            mask = interrupt.bits.msb >> fieldlsb
            gen_const(outstr, defname, 'MASK', mask, existing_defines, True)
            gen_const(outstr, defname, 'OFFSET', fieldlsb, existing_defines)


def gen_const_interrupts(outstr: TextIO,
                         block: IpBlock,
                         component: str,
                         regwidth: int,
                         existing_defines: Set[str]) -> None:
    # If no_auto_intr is true, then we do not generate common defines,
    # because the bit offsets for a particular interrupt may differ between
    # the interrupt enable/state/test registers.
    if block.no_auto_intr:
        return

    genout(outstr, format_comment(first_line("Common Interrupt Offsets")))
    for intr in block.interrupts:
        gen_interrupt_field(outstr, intr, component, regwidth, existing_defines)
    genout(outstr, '\n')


def gen_rust(block: IpBlock,
             outfile: TextIO,
             src_lic: Optional[str],
             src_copy: str) -> int:
    rnames = block.get_rnames()

    outstr = io.StringIO()

    # This tracks the defines that have been generated so far, so we
    # can error if we attempt to duplicate a definition
    existing_defines = set()  # type: Set[str]

    gen_const_module_params(outstr, block, block.name, block.regwidth,
                            existing_defines)

    gen_const_interrupts(outstr, block, block.name, block.regwidth,
                         existing_defines)

    for rb in block.reg_blocks.values():
        for x in rb.entries:
            if isinstance(x, Register):
                gen_const_register(outstr, x, block.name, block.regwidth, rnames,
                                   existing_defines)
                continue

            if isinstance(x, MultiRegister):
                gen_const_multireg(outstr, x, block.name, block.regwidth, rnames,
                                   existing_defines)
                continue

            if isinstance(x, Window):
                gen_const_window(outstr, x, block.name, block.regwidth,
                                 rnames, existing_defines)
                continue

    generated = outstr.getvalue()
    outstr.close()

    genout(outfile, '// Generated register constants for ' + block.name + '\n\n')
    if src_copy != '':
        genout(outfile, '// Copyright information found in source file:\n')
        genout(outfile, '// ' + src_copy + '\n\n')
    if src_lic is not None:
        genout(outfile, '// Licensing information found in source file:\n')
        for line in src_lic.splitlines():
            genout(outfile, '// ' + line + '\n')
        genout(outfile, '\n')

    genout(outfile, generated)

    genout(outfile, '// End generated register constants for ' + block.name)

    return 0


def test_gen_const() -> None:
    outstr = io.StringIO()

    basic_oneline = 'pub const MACRO_NAME 10;\n'
    assert (gen_const(outstr, 'MACRO', 'NAME', 10, set()) == basic_oneline)

    long_macro_name = 'A_VERY_VERY_VERY_VERY_VERY_VERY_VERY_VERY_VERY_VERY_VERY_LONG_MACRO_NAME'
    multiline = ('pub const ' + long_macro_name + ' \\\n' +
                 '  1000000000;\n')

    assert (gen_const(outstr, long_macro_name, '', 1000000000, set()) == multiline)
