#!/usr/bin/env python3

from amaranth import *
from amaranth.sim import *
from amaranth_boards import colorlight_i9
from amaranth_soc.wishbone import *

from minerva.core import Minerva

from typing import List
from argparse import ArgumentParser
from pathlib import Path
import unittest
import sys
import os

from memory import *
from led import *
from eth import *

import i2c
import test_i2c
import uart

# To change clock domain of a module:
# new_thing = DomainRenamer("new_clock")(MyElaboratable())


# TODO clean this up, generate binary here, maybe even run cargo build
def load_firmware_for_mem() -> List[int]:
    with open('../firmware/fw.bin', 'rb') as f:
        # Stored as little endian, LSB first??
        data = f.read()
        out = []
        assert(len(data) % 4 == 0)
        for i in range(int(len(data) / 4)):
            out.append(int.from_bytes(data[i*4:i*4+4], byteorder='little'))

        return out

class Core(Elaboratable):
    def __init__(self, led_signal):
        self.count = Signal(64)
        self.cpu = Minerva(reset_address=0x01000000)
        self.arbiter = Arbiter(addr_width=32, data_width=32)
        self.decoder = Decoder(addr_width=32, data_width=32)
        self.led_signal = led_signal

    def elaborate(self, platform):
        m = Module()
        m.submodules.cpu = self.cpu
        m.submodules.arbiter = self.arbiter
        m.submodules.decoder = self.decoder

        # Connect ibus and dbus together for simplicity for now
        minerva_wb_features = ["cti", "bte", "err"]
        self.ibus = Interface(addr_width=32, data_width=32, features=minerva_wb_features)
        # Note this is a set of statements! without assigning to the comb domain, this will do nothing
        m.d.comb += self.cpu.ibus.connect(self.ibus)
        self.arbiter.add(self.ibus)

        self.dbus = Interface(addr_width=32, data_width=32, features=minerva_wb_features)
        m.d.comb += self.cpu.dbus.connect(self.dbus) # Don't use .eq, use .connect, which will appropriately assign signals
        # using .eq() gave me Multiple Driven errors
        self.arbiter.add(self.dbus)

        # TODO do something with interrupts
        # These are interrupts headed into the CPU
        self.interrupts = Signal(32)
        m.d.comb += [
            # Used for mtime registers, which are memory-mapped (not CSR), so I would have to implement.
            # If I cared.
            self.cpu.timer_interrupt.eq(0),
            # Ostensibly exposed to us so we can interrupt one hart (CPU in this context) from another, we don't need this.
            self.cpu.software_interrupt.eq(0),
            # External interrupt lines, would be for any interrupts I implemented w/ a custom interrupt controller.
            # Most likely I'll map a few lines to some peripherals, won't do anything fancy
            self.cpu.external_interrupt.eq(self.interrupts),
        ]


        fw = load_firmware_for_mem()

        # Hook up memory space
        self.rom = ROM(fw)
        m.submodules.rom = self.rom
        # Problem: not sure to handle how we do byte vs word addressing properly
        # So doing this shift is a bit of a hacky way to impl anything
        start, _stop, _step = self.decoder.add(self.rom, addr=(0x01000000 >> 2))
        print(f"ROM added at 0x{start:08x}")

        self.ram = RAM()
        m.submodules.ram = self.ram
        start, _stop, _step = self.decoder.add(self.ram)
        print(f"RAM added at 0x{start:08x}")

        self.led = LEDPeripheral(self.led_signal)
        m.submodules.led = self.led
        start, _stop, _step = self.decoder.add(self.led)
        print(f"LED added at 0x{start:08x}")

        # Connect arbiter to decoder
        m.d.comb += self.arbiter.bus.connect(self.decoder.bus)

        m.submodules.uart = uart.UART(10e6)

        # Ethernet
        m.submodules.eth = LiteEth()

        # Counter
        #m.d.sync += self.count.eq(self.count + 1)
        #with m.If(self.count >= 50000000):
        #    m.d.sync += self.count.eq(0)
        #    m.d.sync += led.eq(~led)

        return m

class SoC(Elaboratable):
    def __init__(self):
        pass

    def elaborate(self, platform):
        if platform is not None:
            led_signal = platform.request("led")
        else:
            # platform is None in simulation, so provide harnesses for required signals
            led_signal = Signal()

        return Core(led_signal)


# TODO add structure to add regression tests
def run_sim():
    dut = SoC()
    sim = Simulator(dut)

    def proc():
        for i in range(10000):
            yield Tick()

    sim.add_clock(1e-6)
    sim.add_sync_process(proc)
    with sim.write_vcd('test.vcd', gtkw_file='test.gtkw'):
        sim.reset()
        sim.run()


if __name__ == "__main__":
    args = ArgumentParser(description="ARVP Sonar Acquisition FPGA gateware.")
    args.add_argument("--build", action="store_true", help="Build bitstream.")
    args.add_argument("--gen-debug-verilog", action="store_true", help="Save debug verilog.")
    # TODO maybe allow an optional arg to specify an individual test to run?
    args.add_argument("--test", action="store_true", help="Run RTL test suite and report results.")
    args.add_argument("--save-vcd", action="store_true", help="Save VCD waveforms from test(s).")
    args = args.parse_args()

    if args.build:
        colorlight_i9.Colorlight_i9_Platform().build(SoC(), debug_verilog=args.gen_debug_verilog)

    if args.test:
        if args.save_vcd:
            if not Path("vcd_out").exists():
                os.mkdir("vcd_out")

            os.environ["TEST_SAVE_VCD"] = "YES"

        # Super hacky... why am I doing this
        test_modules = [mod for mod in sys.modules if mod.startswith("test_")]
        for mod in test_modules:
            unittest.main(module=mod, argv=[sys.argv[0]])

