from abc import abstractproperty

from ..hdl import *
from ..lib.cdc import ResetSynchronizer
from ..build import *


__all__ = ["LatticeICE40Platform"]


class LatticeICE40Platform(TemplatedPlatform):
    """
    IceStorm toolchain
    ------------------

    Required tools:
        * ``yosys``
        * ``nextpnr-ice40``
        * ``icepack``

    The environment is populated by running the script specified in the environment variable
    ``NMIGEN_ENV_IceStorm``, if present.

    Available overrides:
        * ``verbose``: enables logging of informational messages to standard error.
        * ``read_verilog_opts``: adds options for ``read_verilog`` Yosys command.
        * ``synth_opts``: adds options for ``synth_ice40`` Yosys command.
        * ``script_after_read``: inserts commands after ``read_ilang`` in Yosys script.
        * ``script_after_synth``: inserts commands after ``synth_ice40`` in Yosys script.
        * ``yosys_opts``: adds extra options for ``yosys``.
        * ``nextpnr_opts``: adds extra options for ``nextpnr-ice40``.
        * ``add_pre_pack``: inserts commands at the end in pre-pack Python script.
        * ``add_constraints``: inserts commands at the end in the PCF file.

    Build products:
        * ``{{name}}.rpt``: Yosys log.
        * ``{{name}}.json``: synthesized RTL.
        * ``{{name}}.tim``: nextpnr log.
        * ``{{name}}.asc``: ASCII bitstream.
        * ``{{name}}.bin``: binary bitstream.

    iCECube2 toolchain
    ------------------

    This toolchain comes in two variants: ``LSE-iCECube2`` and ``Synplify-iCECube2``.

    Required tools:
        * iCECube2 toolchain
        * ``tclsh``

    The environment is populated by setting the necessary environment variables based on
    ``NMIGEN_ENV_iCECube2``, which must point to the root of the iCECube2 installation, and
    is required.

    Available overrides:
        * ``verbose``: enables logging of informational messages to standard error.
        * ``lse_opts``: adds options for LSE.
        * ``script_after_add``: inserts commands after ``add_file`` in Synplify Tcl script.
        * ``script_after_options``: inserts commands after ``set_option`` in Synplify Tcl script.
        * ``add_constraints``: inserts commands in SDC file.
        * ``script_after_flow``: inserts commands after ``run_sbt_backend_auto`` in SBT
          Tcl script.

    Build products:
        * ``{{name}}_lse.log`` (LSE) or ``{{name}}_design/{{name}}.htm`` (Synplify): synthesis log.
        * ``sbt/outputs/router/{{name}}_timing.rpt``: timing report.
        * ``{{name}}.edf``: EDIF netlist.
        * ``{{name}}.bin``: binary bitstream.
    """

    toolchain = None # selected when creating platform

    device  = abstractproperty()
    package = abstractproperty()

    # IceStorm templates

    _nextpnr_device_options = {
        "iCE40LP384": "--lp384",
        "iCE40LP1K":  "--lp1k",
        "iCE40LP4K":  "--lp8k",
        "iCE40LP8K":  "--lp8k",
        "iCE40HX1K":  "--hx1k",
        "iCE40HX4K":  "--hx8k",
        "iCE40HX8K":  "--hx8k",
        "iCE40UP5K":  "--up5k",
        "iCE40UP3K":  "--up5k",
        "iCE5LP4K":   "--u4k",
        "iCE5LP2K":   "--u4k",
        "iCE5LP1K":   "--u4k",
    }
    _nextpnr_package_options = {
        "iCE40LP4K":  ":4k",
        "iCE40HX4K":  ":4k",
        "iCE40UP3K":  "",
        "iCE5LP2K":   "",
        "iCE5LP1K":   "",
    }

    _icestorm_required_tools = [
        "yosys",
        "nextpnr-ice40",
        "icepack",
    ]
    _icestorm_file_templates = {
        **TemplatedPlatform.build_script_templates,
        "{{name}}.il": r"""
            # {{autogenerated}}
            {{emit_rtlil()}}
        """,
        "{{name}}.debug.v": r"""
            /* {{autogenerated}} */
            {{emit_debug_verilog()}}
        """,
        "{{name}}.ys": r"""
            # {{autogenerated}}
            {% for file in platform.iter_extra_files(".v") -%}
                read_verilog {{get_override("read_verilog_opts")|options}} {{file}}
            {% endfor %}
            {% for file in platform.iter_extra_files(".sv") -%}
                read_verilog -sv {{get_override("read_verilog_opts")|options}} {{file}}
            {% endfor %}
            {% for file in platform.iter_extra_files(".il") -%}
                read_ilang {{file}}
            {% endfor %}
            read_ilang {{name}}.il
            {{get_override("script_after_read")|default("# (script_after_read placeholder)")}}
            synth_ice40 {{get_override("synth_opts")|options}} -top {{name}}
            {{get_override("script_after_synth")|default("# (script_after_synth placeholder)")}}
            write_json {{name}}.json
        """,
        "{{name}}.pcf": r"""
            # {{autogenerated}}
            {% for port_name, pin_name, attrs in platform.iter_port_constraints_bits() -%}
                set_io {{port_name}} {{pin_name}}
            {% endfor %}
            {% for net_signal, port_signal, frequency in platform.iter_clock_constraints() -%}
                set_frequency {{net_signal|hierarchy(".")}} {{frequency/1000000}}
            {% endfor%}
            {{get_override("add_constraints")|default("# (add_constraints placeholder)")}}
        """,
    }
    _icestorm_command_templates = [
        r"""
        {{invoke_tool("yosys")}}
            {{quiet("-q")}}
            {{get_override("yosys_opts")|options}}
            -l {{name}}.rpt
            {{name}}.ys
        """,
        r"""
        {{invoke_tool("nextpnr-ice40")}}
            {{quiet("--quiet")}}
            {{get_override("nextpnr_opts")|options}}
            --log {{name}}.tim
            {{platform._nextpnr_device_options[platform.device]}}
            --package
                {{platform.package|lower}}{{platform._nextpnr_package_options[platform.device]}}
            --json {{name}}.json
            --pcf {{name}}.pcf
            --asc {{name}}.asc
        """,
        r"""
        {{invoke_tool("icepack")}}
            {{verbose("-v")}}
            {{name}}.asc
            {{name}}.bin
        """
    ]

    # iCECube2 templates

    _icecube2_required_tools = [
        "yosys",
        "synthesis",
        "synpwrap",
        "tclsh",
    ]
    _icecube2_file_templates = {
        **TemplatedPlatform.build_script_templates,
        "build_{{name}}.sh": r"""
            # {{autogenerated}}
            set -e{{verbose("x")}}
            if [ -n "${{platform._toolchain_env_var}}" ]; then
                # LSE environment
                export LD_LIBRARY_PATH=${{platform._toolchain_env_var}}/LSE/bin/lin64:$LD_LIBRARY_PATH
                export PATH=${{platform._toolchain_env_var}}/LSE/bin/lin64:$PATH
                export FOUNDRY=${{platform._toolchain_env_var}}/LSE
                # Synplify environment
                export LD_LIBRARY_PATH=${{platform._toolchain_env_var}}/sbt_backend/bin/linux/opt/synpwrap:$LD_LIBRARY_PATH
                export PATH=${{platform._toolchain_env_var}}/sbt_backend/bin/linux/opt/synpwrap:$PATH
                export SYNPLIFY_PATH=${{platform._toolchain_env_var}}/synpbase
                # Common environment
                export SBT_DIR=${{platform._toolchain_env_var}}/sbt_backend
            else
                echo "Variable ${{platform._toolchain_env_var}} must be set" >&2; exit 1
            fi
            {{emit_commands("sh")}}
        """,
        "{{name}}.v": r"""
            /* {{autogenerated}} */
            {{emit_verilog()}}
        """,
        "{{name}}.debug.v": r"""
            /* {{autogenerated}} */
            {{emit_debug_verilog()}}
        """,
        "{{name}}_lse.prj": r"""
            # {{autogenerated}}
            -a SBT{{platform.family}}
            -d {{platform.device}}
            -t {{platform.package}}
            {{get_override("lse_opts")|options|default("# (lse_opts placeholder)")}}
            {% for file in platform.iter_extra_files(".v") -%}
                -ver {{file}}
            {% endfor %}
            -ver {{name}}.v
            -sdc {{name}}.sdc
            -top {{name}}
            -output_edif {{name}}.edf
            -logfile {{name}}_lse.log
        """,
        "{{name}}_syn.prj": r"""
            # {{autogenerated}}
            {% for file in platform.iter_extra_files(".v", ".sv", ".vhd", ".vhdl") -%}
                add_file -verilog {{file}}
            {% endfor %}
            add_file -verilog {{name}}.v
            add_file -constraint {{name}}.sdc
            {{get_override("script_after_add")|default("# (script_after_add placeholder)")}}
            impl -add {{name}}_design -type fpga
            set_option -technology SBT{{platform.family}}
            set_option -part {{platform.device}}
            set_option -package {{platform.package}}
            {{get_override("script_after_options")|default("# (script_after_options placeholder)")}}
            project -result_format edif
            project -result_file {{name}}.edf
            impl -active {{name}}_design
            project -run compile
            project -run map
            project -run fpga_mapper
            file copy -force -- {{name}}_design/{{name}}.edf {{name}}.edf
        """,
        "{{name}}.sdc": r"""
            # {{autogenerated}}
            {% for net_signal, port_signal, frequency in platform.iter_clock_constraints() -%}
                {% if port_signal is not none -%}
                    create_clock -name {{port_signal.name}} -period {{1000000000/frequency}} [get_ports {{port_signal.name}}]
                {% else -%}
                    create_clock -name {{net_signal.name}} -period {{1000000000/frequency}} [get_nets {{net_signal|hierarchy("/")}}]
                {% endif %}
            {% endfor %}
            {{get_override("add_constraints")|default("# (add_constraints placeholder)")}}
        """,
        "{{name}}.tcl": r"""
            # {{autogenerated}}
            set device {{platform.device}}-{{platform.package}}
            set top_module {{name}}
            set proj_dir .
            set output_dir .
            set edif_file {{name}}
            set tool_options ":edifparser -y {{name}}.pcf"
            set sbt_root $::env(SBT_DIR)
            append sbt_tcl $sbt_root "/tcl/sbt_backend_synpl.tcl"
            source $sbt_tcl
            run_sbt_backend_auto $device $top_module $proj_dir $output_dir $tool_options $edif_file
            {{get_override("script_after_file")|default("# (script_after_file placeholder)")}}
            file copy -force -- sbt/outputs/bitmap/{{name}}_bitmap.bin {{name}}.bin
            exit
        """,
        "{{name}}.pcf": r"""
            # {{autogenerated}}
            {% for port_name, pin_name, attrs in platform.iter_port_constraints_bits() -%}
                set_io {{port_name}} {{pin_name}}
            {% endfor %}
        """,
    }
    _lse_icecube2_command_templates = [
        r"""synthesis -f {{name}}_lse.prj""",
        r"""tclsh {{name}}.tcl""",
    ]
    _synplify_icecube2_command_templates = [
        r"""synpwrap -prj {{name}}_syn.prj -log {{name}}_syn.log""",
        r"""tclsh {{name}}.tcl""",
    ]

    # Common logic

    def __init__(self, *, toolchain="IceStorm"):
        super().__init__()

        assert toolchain in ("IceStorm", "LSE-iCECube2", "Synplify-iCECube2")
        self.toolchain = toolchain

    @property
    def family(self):
        if self.device.startswith("iCE40"):
            return "iCE40"
        if self.device.startswith("iCE5"):
            return "iCE5"
        assert False

    @property
    def _toolchain_env_var(self):
        if self.toolchain == "IceStorm":
            return f"NMIGEN_ENV_{self.toolchain}"
        if self.toolchain in ("LSE-iCECube2", "Synplify-iCECube2"):
            return f"NMIGEN_ENV_iCECube2"
        assert False

    @property
    def required_tools(self):
        if self.toolchain == "IceStorm":
            return self._icestorm_required_tools
        if self.toolchain in ("LSE-iCECube2", "Synplify-iCECube2"):
            return self._icecube2_required_tools
        assert False

    @property
    def file_templates(self):
        if self.toolchain == "IceStorm":
            return self._icestorm_file_templates
        if self.toolchain in ("LSE-iCECube2", "Synplify-iCECube2"):
            return self._icecube2_file_templates
        assert False

    @property
    def command_templates(self):
        if self.toolchain == "IceStorm":
            return self._icestorm_command_templates
        if self.toolchain == "LSE-iCECube2":
            return self._lse_icecube2_command_templates
        if self.toolchain == "Synplify-iCECube2":
            return self._synplify_icecube2_command_templates
        assert False

    def create_missing_domain(self, name):
        # For unknown reasons (no errata was ever published, and no documentation mentions this
        # issue), iCE40 BRAMs read as zeroes for ~3 us after configuration and release of internal
        # global reset. Note that this is a *time-based* delay, generated purely by the internal
        # oscillator, which may not be observed nor influenced directly. For details, see links:
        #  * https://github.com/cliffordwolf/icestorm/issues/76#issuecomment-289270411
        #  * https://github.com/cliffordwolf/icotools/issues/2#issuecomment-299734673
        #
        # To handle this, it is necessary to have a global reset in any iCE40 design that may
        # potentially instantiate BRAMs, and assert this reset for >3 us after configuration.
        # (We add a margin of 5x to allow for PVT variation.) If the board includes a dedicated
        # reset line, this line is ORed with the power on reset.
        #
        # The power-on reset timer counts up because the vendor tools do not support initialization
        # of flip-flops.
        if name == "sync" and self.default_clk is not None:
            clk_i = self.request(self.default_clk).i
            if self.default_rst is not None:
                rst_i = self.request(self.default_rst).i
            else:
                rst_i = Const(0)

            m = Module()

            # Power-on-reset domain
            m.domains += ClockDomain("por", reset_less=True, local=True)
            delay = int(15e-6 * self.default_clk_frequency)
            timer = Signal(range(delay))
            ready = Signal()
            m.d.comb += ClockSignal("por").eq(clk_i)
            with m.If(timer == delay):
                m.d.por += ready.eq(1)
            with m.Else():
                m.d.por += timer.eq(timer + 1)

            # Primary domain
            m.domains += ClockDomain("sync")
            m.d.comb += ClockSignal("sync").eq(clk_i)
            if self.default_rst is not None:
                m.submodules.reset_sync = ResetSynchronizer(~ready | rst_i, domain="sync")
            else:
                m.d.comb += ResetSignal("sync").eq(~ready)

            return m

    def should_skip_port_component(self, port, attrs, component):
        # On iCE40, a differential input is placed by only instantiating an SB_IO primitive for
        # the pin with z=0, which is the non-inverting pin. The pinout unfortunately differs
        # between LP/HX and UP series:
        #  * for LP/HX, z=0 is DPxxB   (B is non-inverting, A is inverting)
        #  * for UP,    z=0 is IOB_xxA (A is non-inverting, B is inverting)
        if attrs.get("IO_STANDARD", "SB_LVCMOS") == "SB_LVDS_INPUT" and component == "n":
            return True
        return False

    def _get_io_buffer(self, m, pin, port, attrs, *, i_invert=False, o_invert=False,
                       invert_lut=False):
        def get_dff(clk, d, q):
            m.submodules += Instance("$dff",
                p_CLK_POLARITY=1,
                p_WIDTH=len(d),
                i_CLK=clk,
                i_D=d,
                o_Q=q)

        def get_ineg(y, invert):
            if invert_lut:
                a = Signal.like(y, name_suffix="_x{}".format(1 if invert else 0))
                for bit in range(len(y)):
                    m.submodules += Instance("SB_LUT4",
                        p_LUT_INIT=Const(0b01 if invert else 0b10, 16),
                        i_I0=a[bit],
                        i_I1=Const(0),
                        i_I2=Const(0),
                        i_I3=Const(0),
                        o_O=y[bit])
                return a
            elif invert:
                a = Signal.like(y, name_suffix="_n")
                m.d.comb += y.eq(~a)
                return a
            else:
                return y

        def get_oneg(a, invert):
            if invert_lut:
                y = Signal.like(a, name_suffix="_x{}".format(1 if invert else 0))
                for bit in range(len(a)):
                    m.submodules += Instance("SB_LUT4",
                        p_LUT_INIT=Const(0b01 if invert else 0b10, 16),
                        i_I0=a[bit],
                        i_I1=Const(0),
                        i_I2=Const(0),
                        i_I3=Const(0),
                        o_O=y[bit])
                return y
            elif invert:
                y = Signal.like(a, name_suffix="_n")
                m.d.comb += y.eq(~a)
                return y
            else:
                return a

        if "GLOBAL" in attrs:
            is_global_input = bool(attrs["GLOBAL"])
            del attrs["GLOBAL"]
        else:
            is_global_input = False
        assert not (is_global_input and i_invert)

        if "i" in pin.dir:
            if pin.xdr < 2:
                pin_i  = get_ineg(pin.i,  i_invert)
            elif pin.xdr == 2:
                pin_i0 = get_ineg(pin.i0, i_invert)
                pin_i1 = get_ineg(pin.i1, i_invert)
        if "o" in pin.dir:
            if pin.xdr < 2:
                pin_o  = get_oneg(pin.o,  o_invert)
            elif pin.xdr == 2:
                pin_o0 = get_oneg(pin.o0, o_invert)
                pin_o1 = get_oneg(pin.o1, o_invert)

        if "i" in pin.dir and pin.xdr == 2:
            i0_ff = Signal.like(pin_i0, name_suffix="_ff")
            i1_ff = Signal.like(pin_i1, name_suffix="_ff")
            get_dff(pin.i_clk, i0_ff, pin_i0)
            get_dff(pin.i_clk, i1_ff, pin_i1)
        if "o" in pin.dir and pin.xdr == 2:
            o1_ff = Signal.like(pin_o1, name_suffix="_ff")
            get_dff(pin.o_clk, pin_o1, o1_ff)

        for bit in range(len(port)):
            io_args = [
                ("io", "PACKAGE_PIN", port[bit]),
                *(("p", key, value) for key, value in attrs.items()),
            ]

            if "i" not in pin.dir:
                # If no input pin is requested, it is important to use a non-registered input pin
                # type, because an output-only pin would not have an input clock, and if its input
                # is configured as registered, this would prevent a co-located input-capable pin
                # from using an input clock.
                i_type =     0b01 # PIN_INPUT
            elif pin.xdr == 0:
                i_type =     0b01 # PIN_INPUT
            elif pin.xdr > 0:
                i_type =     0b00 # PIN_INPUT_REGISTERED aka PIN_INPUT_DDR
            if "o" not in pin.dir:
                o_type = 0b0000   # PIN_NO_OUTPUT
            elif pin.xdr == 0 and pin.dir == "o":
                o_type = 0b0110   # PIN_OUTPUT
            elif pin.xdr == 0:
                o_type = 0b1010   # PIN_OUTPUT_TRISTATE
            elif pin.xdr == 1 and pin.dir == "o":
                o_type = 0b0101   # PIN_OUTPUT_REGISTERED
            elif pin.xdr == 1:
                o_type = 0b1101   # PIN_OUTPUT_REGISTERED_ENABLE_REGISTERED
            elif pin.xdr == 2 and pin.dir == "o":
                o_type = 0b0100   # PIN_OUTPUT_DDR
            elif pin.xdr == 2:
                o_type = 0b1100   # PIN_OUTPUT_DDR_ENABLE_REGISTERED
            io_args.append(("p", "PIN_TYPE", C((o_type << 2) | i_type, 6)))

            if hasattr(pin, "i_clk"):
                io_args.append(("i", "INPUT_CLK",  pin.i_clk))
            if hasattr(pin, "o_clk"):
                io_args.append(("i", "OUTPUT_CLK", pin.o_clk))

            if "i" in pin.dir:
                if pin.xdr == 0 and is_global_input:
                    io_args.append(("o", "GLOBAL_BUFFER_OUTPUT", pin.i[bit]))
                elif pin.xdr < 2:
                    io_args.append(("o", "D_IN_0",  pin_i[bit]))
                elif pin.xdr == 2:
                    # Re-register both inputs before they enter fabric. This increases hold time
                    # to an entire cycle, and adds one cycle of latency.
                    io_args.append(("o", "D_IN_0",  i0_ff[bit]))
                    io_args.append(("o", "D_IN_1",  i1_ff[bit]))
            if "o" in pin.dir:
                if pin.xdr < 2:
                    io_args.append(("i", "D_OUT_0", pin_o[bit]))
                elif pin.xdr == 2:
                    # Re-register negedge output after it leaves fabric. This increases setup time
                    # to an entire cycle, and doesn't add latency.
                    io_args.append(("i", "D_OUT_0", pin_o0[bit]))
                    io_args.append(("i", "D_OUT_1", o1_ff[bit]))

            if pin.dir in ("oe", "io"):
                io_args.append(("i", "OUTPUT_ENABLE", pin.oe))

            if is_global_input:
                m.submodules["{}_{}".format(pin.name, bit)] = Instance("SB_GB_IO", *io_args)
            else:
                m.submodules["{}_{}".format(pin.name, bit)] = Instance("SB_IO", *io_args)

    def get_input(self, pin, port, attrs, invert):
        self._check_feature("single-ended input", pin, attrs,
                            valid_xdrs=(0, 1, 2), valid_attrs=True)
        m = Module()
        self._get_io_buffer(m, pin, port, attrs, i_invert=invert)
        return m

    def get_output(self, pin, port, attrs, invert):
        self._check_feature("single-ended output", pin, attrs,
                            valid_xdrs=(0, 1, 2), valid_attrs=True)
        m = Module()
        self._get_io_buffer(m, pin, port, attrs, o_invert=invert)
        return m

    def get_tristate(self, pin, port, attrs, invert):
        self._check_feature("single-ended tristate", pin, attrs,
                            valid_xdrs=(0, 1, 2), valid_attrs=True)
        m = Module()
        self._get_io_buffer(m, pin, port, attrs, o_invert=invert)
        return m

    def get_input_output(self, pin, port, attrs, invert):
        self._check_feature("single-ended input/output", pin, attrs,
                            valid_xdrs=(0, 1, 2), valid_attrs=True)
        m = Module()
        self._get_io_buffer(m, pin, port, attrs, i_invert=invert, o_invert=invert)
        return m

    def get_diff_input(self, pin, p_port, n_port, attrs, invert):
        self._check_feature("differential input", pin, attrs,
                            valid_xdrs=(0, 1, 2), valid_attrs=True)
        m = Module()
        # See comment in should_skip_port_component above.
        self._get_io_buffer(m, pin, p_port, attrs, i_invert=invert)
        return m

    def get_diff_output(self, pin, p_port, n_port, attrs, invert):
        self._check_feature("differential output", pin, attrs,
                            valid_xdrs=(0, 1, 2), valid_attrs=True)
        m = Module()
        # Note that the non-inverting output pin is not driven the same way as a regular
        # output pin. The inverter introduces a delay, so for a non-inverting output pin,
        # an identical delay is introduced by instantiating a LUT. This makes the waveform
        # perfectly symmetric in the xdr=0 case.
        self._get_io_buffer(m, pin, p_port, attrs, o_invert=    invert, invert_lut=True)
        self._get_io_buffer(m, pin, n_port, attrs, o_invert=not invert, invert_lut=True)
        return m

    # Tristate and bidirectional buffers are not supported on iCE40 because it requires external
    # termination, which is incompatible for input and output differential I/Os.

    # CDC primitives are not currently specialized for iCE40. It is not known if iCECube2 supports
    # the necessary attributes; nextpnr-ice40 does not.
