library IEEE;
use IEEE.Std_Logic_1164.all;
use IEEE.Numeric_Std.all;
use WORK.MokuSupport.all;

entity SPIMemoryMap is
    generic (
        NUM_REGS : integer := 128;
        MMAP_BITS : integer := 20;
        -- Add one Clk delay to the write strobes, typically
        -- needed when the registers are multicylce.
        STROBE_DELAY : boolean := false
    );
    port (
        Clk : in std_logic;
        Reset : in std_logic;

        MISO : out std_logic;
        MOSI : in std_logic;
        SClk : in std_logic;  -- SClk < Clk / 4
        CS : in std_logic;

        Control : out ControlRegisterArray(0 to NUM_REGS - 1);
        Status : in StatusRegisterArray(0 to NUM_REGS - 1);

        MiscWriteStrobe : out std_logic;
        MiscWriteData : out std_logic_vector(31 downto 0);
        MiscWriteAddress : out std_logic_vector(MMAP_BITS - 1 downto 0);
        MiscReadStrobe : out std_logic;
        MiscReadData : in std_logic_vector(31 downto 0) := (others => '0');
        MiscReadAddress : out std_logic_vector(MMAP_BITS - 1 downto 0)
    );
end entity;

architecture Behavioural of SPIMemoryMap is
    type State_T is (ADDR, DATA);
    signal State, NextState : State_T;

    signal CSSync, SClkSync, MOSISync : std_logic;
    signal BitCount : unsigned(4 downto 0);
    signal RiseEdge, FallEdge : std_logic;
    signal MISOBuf, MOSIBuf, BEMOSIBuf : std_logic_vector(31 downto 0);
    signal AddrAccum : unsigned(MMAP_BITS - 3 downto 0);  -- ignore two least significant bits to align with 4 byte words
    signal DoWrite : std_logic;  -- write transactions (first bit received during SPI transaction)
    signal DoMisc : std_logic;  -- misc port access (topmost address bit)

    type SLV32Array is array (natural range <>) of std_logic_vector(31 downto 0);
    signal Value : SLV32Array(Control'range);
    signal WriteStrobe, WriteStrobeDly : std_logic_vector(NUM_REGS - 1 downto 0);
    signal ReadStrobe : std_logic_vector(NUM_REGS - 1 downto 0);
    signal WriteEvt : std_logic;
    signal ReadEvt : std_logic;
    signal RiseDly1, RiseDly2, RiseDly3 : std_logic;
    signal CtrlIndex : integer;
    signal MiscReadBuf, StatusBuf : std_logic_vector(31 downto 0);

    function ChangeEndian32(X: std_logic_vector(31 downto 0)) return std_logic_vector is
        variable Z : std_logic_vector(31 downto 0);
    begin
        Z(31 downto 24) := X( 7 downto  0);
        Z(23 downto 16) := X(15 downto  8);
        Z(15 downto  8) := X(23 downto 16);
        Z( 7 downto  0) := X(31 downto 24);
        return Z;
    end function;
begin

    EDGES: process(Clk) is
        variable SClkDly : std_logic;
    begin
        if rising_edge(Clk) then
            RiseEdge <= SClkSync and not SClkDly;
            FallEdge <= not SClkSync and SClkDly;
            SClkDly := SClkSync;

            RiseDly3 <= RiseDly2;
            RiseDly2 <= RiseDly1;
            RiseDly1 <= RiseEdge;

            CSSync <= CS;
            SClkSync <= SClk;
            MOSISync <= MOSI;
        end if;
    end process;

    process(Clk) is
    begin
        if rising_edge(Clk) then
            if Reset ='0' and CSSync = '0' then
                if FallEdge = '1' then
                    BitCount <= BitCount + 1;
                end if;
            else
                BitCount <= (others => '0');
            end if;

            if Reset = '1' or CSSync = '1' then
                State <= ADDR;
            else
                State <= NextState;
            end if;
        end if;
    end process;

    process(State, BitCount, FallEdge, CSSync) is
    begin
        case State is
            when ADDR =>
                if FallEdge = '1' and BitCount = 31 then
                    NextState <= DATA;
                else
                    NextState <= ADDR;
                end if;
            when DATA =>
                if CSSync = '1' then
                    NextState <= ADDR;
                else
                    NextState <= DATA;
                end if;
        end case;
    end process;

    MISO <= MISOBuf(MISOBuf'left);
    BEMOSIBuf <= ChangeEndian32(MOSIBuf);

    ADDR_LATCH: process(Clk) is
    begin
        if rising_edge(Clk) then
            if RiseDly1 = '1' and BitCount = 31 then
                if State = ADDR then
                    -- TODO don't need 24bit addr for 32bit words
                    AddrAccum <= unsigned(BEMOSIBuf(MMAP_BITS - 1 downto 2));
                    DoWrite <= BEMOSIBuf(BEMOSIBuf'left);
                    DoMisc <= BEMOSIBuf(MMAP_BITS);
                elsif State = DATA then
                    AddrAccum <= AddrAccum + 1;
                end if;
            end if;
        end if;
    end process;

    -- This is just to make the simulator happy
    CtrlIndex <= to_integer(AddrAccum) when AddrAccum < NUM_REGS else 0;

    WRITE_LATCH: process(Clk) is
    begin
        if rising_edge(Clk) then
            -- Clear all WriteStrobes
            for I in WriteStrobe'range loop
                WriteStrobe(I) <= '0';
            end loop;

            WriteStrobe(CtrlIndex) <= WriteEvt and not DoMisc;
            WriteStrobeDly <= WriteStrobe;

            if Reset = '1' then
                for I in Value'range loop
                    Value(I) <= (others => '0');
                end loop;
            elsif WriteEvt = '1' and DoMisc = '0' then
                Value(CtrlIndex) <= BEMOSIBuf;
            end if;

            for I in ReadStrobe'range loop
                ReadStrobe(I) <= '0';
            end loop;

            ReadStrobe(CtrlIndex) <= ReadEvt and not DoMisc;
        end if;
    end process;

    process(Clk) is
    begin
        if rising_edge(Clk) then
            if RiseDly3 = '1' then
                StatusBuf <= Status(CtrlIndex);
                MiscReadBuf <= MiscReadData;
            end if;

            if WriteEvt = '1' and DoMisc = '1' then
                MiscWriteAddress <= std_logic_vector("00" & AddrAccum);
                MiscWriteData <= BEMOSIBuf;
                MiscWriteStrobe <= '1';
            else
                MiscWriteStrobe <= '0';
            end if;
        end if;
    end process;

    BUF_SHIFT: process(Clk) is
    begin
        if rising_edge(Clk) then
            if FallEdge = '1' then
                if BitCount = 31 then
                    if DoMisc = '0' then
                        MISOBuf <= ChangeEndian32(StatusBuf);
                    else
                        MISOBuf <= ChangeEndian32(MiscReadBuf);
                    end if;
                else
                    MISOBuf <= MISOBuf(MISOBuf'left - 1 downto 0) & '0';
                end if;
            end if;

            if RiseEdge = '1' then
                MOSIBuf <= MOSIBuf(MOSIBuf'left - 1 downto 0) & MOSISync;
            end if;
        end if;
    end process;

    MiscReadAddress <= std_logic_vector("00" & AddrAccum);
    MiscReadStrobe <= ReadEvt and DoMisc;

    WriteEvt <= DoWrite and RiseDly1 when State = DATA and BitCount = 31 else '0';
    ReadEvt <= not DoWrite and RiseDly2 when BitCount = 31 else '0';

    CTRL_OUT: for I in Control'range generate
        Control(I).Value <= Value(I);
        Control(I).WriteStrobe <= WriteStrobeDly(I) when STROBE_DELAY else WriteStrobe(I);
        Control(I).ReadStrobe <= ReadStrobe(I);
    end generate;
end architecture;
