"""
===============
Code Generation
===============

Once an :class:`~b_asic.architecture.Architecture` has been derived, the final
step is to generate synthesizable HDL that describes it, along with a testbench
that can be used to verify the implementation through simulation.
"""

# %%
# Recreating the Architecture
# ---------------------------
# We start by reproducing the architecture derived in the previous tutorial.
from scipy.signal import iirfilter

from b_asic.architecture import Architecture, Memory, ProcessingElement
from b_asic.schedule import Schedule
from b_asic.sfg_generators.wave_digital_filters import lattice_wdf
from b_asic.special_operations import Input, Output
from b_asic.wdf import lattice_coeffs_from_tf
from b_asic.wdf_operations import SymmetricTwoportAdaptor
from b_asic.quantization import quantize

b, a = iirfilter(N=7, Wn=0.3, rp=0.1, rs=60, btype="low", ftype="ellip")
adaptor_coeffs = lattice_coeffs_from_tf(a)
quantized_coeffs = [quantize(c, fractional_bits=14) for c in adaptor_coeffs]

wdf_sfg = lattice_wdf(quantized_coeffs, only_adaptors=True)
wdf_sfg.set_latency_of_type(SymmetricTwoportAdaptor, 4)
wdf_sfg.set_execution_time_of_type(SymmetricTwoportAdaptor, 1)

schedule = Schedule(wdf_sfg, cyclic=True)
schedule.move_operation('out0', 9)
schedule.move_operation('sym2p3', 4)
schedule.move_operation('sym2p3', 5)
schedule.move_operation('sym2p4', 1)
schedule.move_operation('sym2p6', 4)
schedule.move_operation('sym2p4', 7)
schedule.move_operation('sym2p1', 1)
schedule.set_schedule_time(12)
schedule.move_operation('sym2p1', 1)
schedule.move_operation('sym2p6', 1)
schedule.move_operation('sym2p5', 2)
schedule.move_operation('sym2p2', 2)
schedule.move_operation('sym2p7', 1)
schedule.move_operation('sym2p6', -1)
schedule.move_operation('sym2p4', -2)
schedule.move_operation('sym2p5', -1)
schedule.move_operation('sym2p4', -1)
schedule.move_operation('sym2p3', -4)
schedule.move_operation('sym2p6', -1)
schedule.move_operation('out0', -4)
schedule.move_operation('sym2p1', 1)
schedule.move_operation('sym2p2', 1)
schedule.move_operation('sym2p6', 1)
schedule.move_y_location('sym2p3', 9, True)
schedule.move_operation('out0', 1)
schedule.move_operation('sym2p3', 1)
schedule.move_operation('sym2p4', 1)
schedule.move_operation('sym2p4', -1)
schedule.move_operation('sym2p6', -3)
schedule.move_operation('sym2p6', 3)
schedule.move_operation('sym2p6', -2)
schedule.move_y_location('sym2p6', 3, True)
schedule.move_operation('sym2p6', 2)
schedule.move_y_location('sym2p6', 5, True)

variables = schedule.get_memory_variables()
direct, variables = variables.split_on_length()
variable_groups = variables.split_on_ports(write_ports=1, read_ports=1, total_ports=2)
mem0 = Memory(variable_groups[0], assign=True, entity_name="m0")
mem1 = Memory(variable_groups[1], assign=True, entity_name="m1")

ops = schedule.get_operations()
adaptor = ProcessingElement(ops.get_by_type(SymmetricTwoportAdaptor), entity_name="a")
input_pe = ProcessingElement(ops.get_by_type(Input), entity_name="x")
output_pe = ProcessingElement(ops.get_by_type(Output), entity_name="y")

arch = Architecture(
    processing_elements=[adaptor, input_pe, output_pe],
    memories=[mem0, mem1],
    direct_interconnects=direct,
    entity_name="wdf",
)

# %%
# Generating VHDL
# ---------------
# HDL is generated by a code printer.
# Currently, VHDL is supported, which is done through
# :class:`~b_asic.code_printer.vhdl.vhdl_printer.VhdlPrinter`.
# The printer needs to know which :class:`~b_asic.data_type.DataType` to use
# for the generated arithmetic.
# Here, we use 10 fractional bits, and two integer bits.
# Furthermore, we use magnitude truncation for quantization,
# and saturation for overflow handling.
from b_asic.code_printer import VhdlPrinter
from b_asic.data_type import DataType
from b_asic.quantization import QuantizationMode, OverflowMode

dt = DataType(
    wl=(2, 10),
    quantization_mode=QuantizationMode.MAGNITUDE_TRUNCATION,
    overflow_mode=OverflowMode.SATURATION,
)
printer = VhdlPrinter(dt)

# %%
# Generating the VHDL files describing the architecture is then as simple as
printer.print(arch, path="generated")

# %%
# This writes one file per processing element, I/O unit, and memory, along with a
# top-level file describing ``arch`` itself.
# :meth:`~b_asic.code_printer.vhdl.vhdl_printer.VhdlPrinter.get_compile_order` returns the
# resulting file names in the order in which they can be compiled.
print(printer.get_compile_order(arch))

# %%
# Let's have a look at the generated code,
# by reading back the file that was written.
from pathlib import Path

# %%
# Top-level file, containing all units found below, top-level muxes, and
# the schedule counter.
print(Path("generated", "wdf.vhdl").read_text())

# %%
# Memories
print(Path("generated", "m0.vhdl").read_text())
print(Path("generated", "m1.vhdl").read_text())

# %%
# Adaptor PE
print(Path("generated", "a.vhdl").read_text())

# %%
# I/O units
print(Path("generated", "x.vhdl").read_text())
print(Path("generated", "y.vhdl").read_text())

# %%
# A number of keyword arguments can be passed to
# :meth:`~b_asic.code_printer.vhdl.vhdl_printer.VhdlPrinter.print` to alter the generated code,
# e.g., to add registers on I/O ports, pipeline mux control signals for the top-level,
# or forcing a certain register-placement for PEs.

# %%
# Generating a Testbench
# ----------------------
# To verify that the generated VHDL behaves as expected, a testbench can be
# generated from a finite-wordlength :class:`~b_asic.simulation.Simulation` of
# the SFG, using the same :class:`~b_asic.data_type.DataType` as for the
# code generation.
# Here, we drive the filter with a uniform input sequence for a few
# repetitions of the schedule period.
# Note that we use :attr:`~b_asic.schedule.Schedule.sfg` here,
# which is the recreated SFG after scheduling.
# This is because, the scheduling pipelined/retimed the algorithm.
from b_asic.signal_generator import Uniform
from b_asic.simulation import Simulation

sim = Simulation(schedule.sfg, [Uniform()], dt)
sim.run_for(10 * schedule.schedule_time)

# %%
# The simulation results are then handed to a
# :class:`~b_asic.tb_printer.CocotbPrinter`, which generates a
# `cocotb <https://www.cocotb.org/>`_ testbench that drives the architecture
# and asserts that its outputs match the simulated values.
# If a VHDL testbench is preferred, :class:`~b_asic.tb_printer.VhdlTbPrinter` is also available.
from b_asic.tb_printer import CocotbPrinter

cocotb_printer = CocotbPrinter(sim.results)
cocotb_printer.print(arch, schedule, path="generated", simulator = "ghdl", waves=True)

# %%
# This writes a ``tb.py`` file containing the testbench.
# Let's have a look at the generated testbench.
print(Path("generated", "tb.py").read_text())

# %%
# Assuming that GHDL is correctly set up, verification should be as simple as
# running
#
# .. code-block:: bash
#
#     python tb.py
#
# in the ``generated`` directory, which should run the testbench and report
# that all assertions passed.
# A waveform file is also generated, which can be rendered using a waveform viewer, e.g., `Surfer <https://surfer-project.org>`_.

# %%
# Generating a VHDL LS Configuration
# ----------------------------------
# To get clickable references, completion, and other IDE features for the generated
# VHDL code, e.g., through the
# `VHDL by VHDL-LS <https://marketplace.visualstudio.com/items?itemName=hbohlin.vhdl-ls>`_
# VS Code extension, a ``vhdl_ls.toml`` configuration file for the
# `VHDL Language Server <https://github.com/VHDL-LS/rust_hdl>`_ can be
# generated with
# :meth:`~b_asic.code_printer.vhdl.vhdl_printer.VhdlPrinter.print_vhdl_ls_toml`.
printer.print_vhdl_ls_toml(arch, path="generated")

# %%
# Let's have a look at the generated file.
print(Path("generated", "vhdl_ls.toml").read_text())

# %%
# Conclusion
# ----------
# Congratualations, you have now successfully designed a wave digital filter,
# derived a time-multiplexed architecture for it,
# and generated synthesizable VHDL code, along with a testbench for it.
# The generated code can now be synthesized and implemented on an FPGA or ASIC.

# %%
# Next Steps
# ----------
# Take a look at :ref:`examples` to see B-ASIC used in more complex situations,
# play around with a different schedules and assignments for the current algorithm,
# or try building your own architecture for a different algorithm.
# If you encounter any issues, or have suggestions for improvements, please open an issue on
# `GitHub <https://github.com/b-asic-eda/b-asic/issues>`_.
