module n64_pi (
    input clk,
    input reset,

    mem_bus.controller mem_bus,
    n64_reg_bus.controller reg_bus,

    n64_scb.pi n64_scb,

    input n64_reset,
    input n64_nmi,
    input n64_pi_alel,
    input n64_pi_aleh,
    input n64_pi_read,
    input n64_pi_write,
    inout [15:0] n64_pi_ad
);

    // Control signals and input synchronization

    logic [1:0] n64_reset_ff;
    logic [1:0] n64_nmi_ff;
    logic [3:0] n64_pi_alel_ff;
    logic [3:0] n64_pi_aleh_ff;
    logic [1:0] n64_pi_read_ff;
    logic [2:0] n64_pi_write_ff;

    always_ff @(posedge clk) begin
        n64_reset_ff <= {n64_reset_ff[0], n64_reset};
        n64_nmi_ff <= {n64_nmi_ff[0], n64_nmi};
        n64_pi_aleh_ff <= {n64_pi_aleh_ff[2:0], n64_pi_aleh};
        n64_pi_alel_ff <= {n64_pi_alel_ff[2:0], n64_pi_alel};
        n64_pi_read_ff <= {n64_pi_read_ff[0], n64_pi_read};
        n64_pi_write_ff <= {n64_pi_write_ff[1:0], n64_pi_write};
    end

    logic pi_reset;
    logic pi_nmi;
    logic pi_aleh;
    logic pi_alel;
    logic pi_read;
    logic pi_write;

    always_comb begin
        pi_reset = n64_reset_ff[1];
        pi_nmi = n64_nmi_ff[1];
        pi_aleh = n64_pi_aleh_ff[3];
        pi_alel = n64_pi_alel_ff[3];
        pi_read = n64_pi_read_ff[1];
        pi_write = n64_pi_write_ff[2];
    end


    // PI bus state and event generator

    typedef enum bit [1:0] {
        PI_MODE_IDLE    = 2'b10,
        PI_MODE_HIGH    = 2'b11,
        PI_MODE_LOW     = 2'b01,
        PI_MODE_VALID   = 2'b00
    } e_pi_mode;

    typedef enum bit [1:0] {
        PORT_NONE,
        PORT_MEM,
        PORT_REG
    } e_port;

    e_pi_mode pi_mode;

    e_port read_port;
    e_port write_port;

    always_comb begin
        pi_mode = e_pi_mode'({pi_aleh, pi_alel});
    end

    logic last_reset;
    logic last_nmi;
    e_pi_mode last_pi_mode;
    logic last_read;
    logic last_write;

    always_ff @(posedge clk) begin
        last_reset <= pi_reset;
        last_nmi <= pi_nmi;
        last_pi_mode <= pi_mode;
        last_read <= pi_read;
        last_write <= pi_write;
    end

    logic aleh_op;
    logic alel_op;
    logic read_op;
    logic write_op;
    logic end_op;

    always_comb begin
        n64_scb.n64_reset = !last_reset && pi_reset;
        n64_scb.n64_nmi = !last_nmi && pi_nmi;
        aleh_op = pi_reset && (last_pi_mode != PI_MODE_HIGH) && (pi_mode == PI_MODE_HIGH);
        alel_op = pi_reset && (last_pi_mode == PI_MODE_HIGH) && (pi_mode == PI_MODE_LOW);
        read_op = pi_reset && (pi_mode == PI_MODE_VALID) && (read_port != PORT_NONE) && (last_read && !pi_read);
        write_op = pi_reset && (pi_mode == PI_MODE_VALID) && (write_port != PORT_NONE) && (last_write && !pi_write);
        end_op = pi_reset && (last_pi_mode == PI_MODE_VALID) && (pi_mode != PI_MODE_VALID);
    end


    // Input and output data sampling

    logic n64_pi_ad_oe;
    logic [15:0] n64_pi_ad_out;
    logic [15:0] n64_pi_dq_in;
    logic [15:0] n64_pi_dq_out;

    assign n64_pi_ad = n64_pi_ad_oe ? n64_pi_ad_out : 16'hZZZZ;

    always_ff @(posedge clk) begin
        n64_pi_ad_oe <= pi_reset && (pi_mode == PI_MODE_VALID) && !last_read && (read_port != PORT_NONE);
        n64_pi_ad_out <= n64_pi_dq_out;
        n64_pi_dq_in <= n64_pi_ad;
    end


    // Debug: last accessed PI address

    always_ff @(posedge clk) begin
        if (aleh_op) begin
            n64_scb.pi_debug[31:16] <= n64_pi_dq_in;
        end
        if (alel_op) begin
            n64_scb.pi_debug[15:0] <= n64_pi_dq_in;
        end
    end


    // Address decoding

    const bit [31:0] DDIPL_OFFSET       = 32'h03BC_0000;
    const bit [31:0] SAVE_OFFSET        = 32'h03FE_0000;
    const bit [31:0] FLASH_OFFSET       = 32'h0400_0000;
    const bit [31:0] BOOTLOADER_OFFSET  = 32'h04E0_0000;
    const bit [31:0] SHADOW_OFFSET      = 32'h04FE_0000;
    const bit [31:0] BUFFER_OFFSET      = 32'h0500_0000;

    logic [31:0] mem_offset;
    logic sram_selected;

    always_ff @(posedge clk) begin
        if (reset || !pi_reset || end_op) begin
            n64_scb.pi_sdram_active <= 1'b0;
            n64_scb.pi_flash_active <= 1'b0;
        end

        if (reset) begin
            read_port <= PORT_NONE;
            write_port <= PORT_NONE;
            sram_selected <= 1'b0;
            reg_bus.dd_select <= 1'b0;
            reg_bus.flashram_select <= 1'b0;
            reg_bus.cfg_select <= 1'b0;
        end else if (aleh_op) begin
            read_port <= PORT_NONE;
            write_port <= PORT_NONE;
            mem_offset <= 32'd0;
            sram_selected <= 1'b0;
            reg_bus.dd_select <= 1'b0;
            reg_bus.flashram_select <= 1'b0;
            reg_bus.cfg_select <= 1'b0;

            if (n64_scb.dd_enabled) begin
                if (n64_pi_dq_in == 16'h0500) begin
                    read_port <= PORT_REG;
                    write_port <= PORT_REG;
                    reg_bus.dd_select <= 1'b1;
                end
            end

            if (n64_scb.ddipl_enabled) begin
                if (n64_pi_dq_in >= 16'h0600 && n64_pi_dq_in < 16'h0640) begin
                    read_port <= PORT_MEM;
                    write_port <= PORT_NONE;
                    mem_offset <= (-32'h0600_0000) + DDIPL_OFFSET;
                    n64_scb.pi_sdram_active <= 1'b1;
                end
            end

            if (n64_scb.flashram_enabled) begin
                if (n64_pi_dq_in >= 16'h0800 && n64_pi_dq_in < 16'h0802) begin
                    read_port <= PORT_REG;
                    write_port <= PORT_REG;
                    mem_offset <= (-32'h0800_0000) + SAVE_OFFSET;
                    reg_bus.flashram_select <= 1'b1;
                    if (n64_scb.flashram_read_mode) begin
                        read_port <= PORT_MEM;
                        n64_scb.pi_sdram_active <= 1'b1;
                    end
                end
            end else if (n64_scb.sram_enabled) begin
                if (n64_scb.sram_banked) begin
                    if (n64_pi_dq_in >= 16'h0800 && n64_pi_dq_in < 16'h0810) begin
                        if (n64_pi_dq_in[3:2] != 2'b11 && n64_pi_dq_in[1:0] == 2'b00) begin
                            read_port <= PORT_MEM;
                            write_port <= PORT_MEM;
                            mem_offset <= (-32'h0800_0000) - {n64_pi_dq_in[3:2], 18'd0} + {n64_pi_dq_in[3:2], 15'd0} + SAVE_OFFSET;
                            sram_selected <= 1'b1;
                            n64_scb.pi_sdram_active <= 1'b1;
                        end
                    end
                end else begin
                    if (n64_pi_dq_in >= 16'h0800 && n64_pi_dq_in < 16'h0802) begin
                        read_port <= PORT_MEM;
                        write_port <= PORT_MEM;
                        mem_offset <= (-32'h0800_0000) + SAVE_OFFSET;
                        sram_selected <= 1'b1;
                        n64_scb.pi_sdram_active <= 1'b1;
                    end
                end
            end

            if (n64_scb.bootloader_enabled) begin
                if (n64_pi_dq_in >= 16'h1000 && n64_pi_dq_in < 16'h101E) begin
                    read_port <= PORT_MEM;
                    write_port <= PORT_NONE;
                    mem_offset <= (-32'h1000_0000) + BOOTLOADER_OFFSET;
                    n64_scb.pi_flash_active <= 1'b1;
                end
            end else begin
                if (n64_pi_dq_in >= 16'h1000 && n64_pi_dq_in < 16'h1400) begin
                    read_port <= PORT_MEM;
                    write_port <= n64_scb.rom_write_enabled ? PORT_MEM : PORT_NONE;
                    mem_offset <= (-32'h1000_0000);
                    n64_scb.pi_sdram_active <= 1'b1;
                end
            end

            if (n64_scb.rom_shadow_enabled) begin
                if (n64_pi_dq_in >= 16'h13FE && n64_pi_dq_in < 16'h1400) begin
                    read_port <= PORT_MEM;
                    write_port <= PORT_NONE;
                    mem_offset <= (-32'h13FE_0000) + SHADOW_OFFSET;
                    n64_scb.pi_flash_active <= 1'b1;
                end
            end

            if (n64_scb.rom_extended_enabled) begin
                if (n64_pi_dq_in >= 16'h1400 && n64_pi_dq_in < 16'h14E0) begin
                    read_port <= PORT_MEM;
                    write_port <= PORT_NONE;
                    mem_offset <= (-32'h1400_0000) + FLASH_OFFSET;
                    n64_scb.pi_flash_active <= 1'b1;
                end
            end

            if (n64_scb.cfg_unlock) begin
                if (n64_pi_dq_in >= 16'h1FFC && n64_pi_dq_in < 16'h1FFE) begin
                    read_port <= PORT_MEM;
                    write_port <= PORT_NONE;
                    mem_offset <= (-32'h1FFC_0000) + SHADOW_OFFSET;
                    n64_scb.pi_flash_active <= 1'b1;
                end

                if (n64_pi_dq_in >= 16'h1FFE && n64_pi_dq_in < 16'h1FFF) begin
                    read_port <= PORT_MEM;
                    write_port <= PORT_MEM;
                    mem_offset <= (-32'h1FFE_0000) + BUFFER_OFFSET;
                end
            end

            if (n64_pi_dq_in >= 16'h1FFF && n64_pi_dq_in < 16'h2000) begin
                read_port <= n64_scb.cfg_unlock ? PORT_REG : PORT_NONE;
                write_port <= PORT_REG;
                reg_bus.cfg_select <= 1'b1;
            end
        end
    end


    // Mem bus read FIFO controller

    logic read_fifo_full;
    logic read_fifo_write;
    logic [15:0] read_fifo_wdata;

    logic read_fifo_empty;
    logic read_fifo_read;
    logic [15:0] read_fifo_rdata;

    logic read_fifo_wait;

    n64_pi_fifo read_fifo_inst (
        .clk(clk),
        .reset(reset),

        .flush(reset || !pi_reset || alel_op),
        
        .full(read_fifo_full),
        .write(read_fifo_write),
        .wdata(read_fifo_wdata),
        
        .empty(read_fifo_empty),
        .read(read_fifo_read),
        .rdata(read_fifo_rdata)
    );

    always_ff @(posedge clk) begin
        read_fifo_read <= 1'b0;

        if (!pi_reset) begin
            n64_scb.pi_debug[33:32] <= 2'b00;
        end

        if (reset || !pi_reset || alel_op) begin
            read_fifo_wait <= 1'b0;
        end

        if (read_port == PORT_MEM) begin
            if (read_op) begin
                if (read_fifo_empty) begin
                    read_fifo_wait <= 1'b1;
                    n64_scb.pi_debug[32] <= 1'b1;
                    if (read_fifo_wait) begin
                        n64_scb.pi_debug[33] <= 1'b1;
                    end
                end else begin
                    read_fifo_read <= 1'b1;
                    n64_pi_dq_out <= read_fifo_rdata;
                end
            end

            if (!read_fifo_empty && read_fifo_wait) begin
                read_fifo_read <= 1'b1;
                read_fifo_wait <= 1'b0;
                n64_pi_dq_out <= read_fifo_rdata;
            end
        end

        if (read_port == PORT_REG) begin
            if (read_op) begin
                n64_pi_dq_out <= reg_bus.rdata;
            end
        end
    end


    // Mem bus write FIFO controller

    logic write_fifo_full;
    logic write_fifo_write;
    logic [15:0] write_fifo_wdata;

    logic write_fifo_empty;
    logic write_fifo_read;
    logic [15:0] write_fifo_rdata;

    logic write_fifo_wait;

    n64_pi_fifo write_fifo_inst (
        .clk(clk),
        .reset(reset),

        .flush(reset),

        .full(write_fifo_full),
        .write(write_fifo_write),
        .wdata(write_fifo_wdata),

        .empty(write_fifo_empty),
        .read(write_fifo_read),
        .rdata(write_fifo_rdata)
    );

    always_ff @(posedge clk) begin
        write_fifo_write <= 1'b0;

        if (!pi_reset) begin
            n64_scb.pi_debug[35:34] <= 2'b00;
        end

        if (reset) begin
            write_fifo_wait <= 1'b0;
        end

        if (write_port == PORT_MEM) begin
            if (write_op) begin
                if (write_fifo_full) begin
                    write_fifo_wait <= 1'b1;
                    n64_scb.pi_debug[34] <= 1'b1;
                    if (write_fifo_wait) begin
                        n64_scb.pi_debug[35] <= 1'b1;
                    end
                end else begin
                    write_fifo_write <= 1'b1;
                    write_fifo_wdata <= n64_pi_dq_in;
                end
            end

            if (!write_fifo_full && write_fifo_wait) begin
                write_fifo_write <= 1'b1;
                write_fifo_wait <= 1'b0;
                write_fifo_wdata <= n64_pi_dq_in;
            end
        end
    end


    // Mem bus controller

    logic [31:0] starting_address;
    logic load_starting_address;
    logic read_enabled;
    logic first_write_op;

    always_ff @(posedge clk) begin
        write_fifo_read <= 1'b0;
        load_starting_address <= 1'b0;
        n64_scb.sram_done <= 1'b0;

        if (reset || !pi_reset) begin
            mem_bus.request <= 1'b0;
            read_enabled <= 1'b0;
        end else begin
            if (aleh_op) begin
                starting_address[31:16] <= n64_pi_dq_in;
            end

            if (alel_op) begin
                starting_address <= {starting_address[31:16], n64_pi_dq_in} + mem_offset;
                load_starting_address <= 1'b1;
                read_enabled <= 1'b1;
                first_write_op <= 1'b1;
            end

            if (load_starting_address) begin
                mem_bus.address <= starting_address;
            end

            if (!mem_bus.request) begin
                if ((write_port == PORT_MEM) && !write_fifo_empty) begin
                    mem_bus.request <= 1'b1;
                    mem_bus.write <= 1'b1;
                    mem_bus.wdata <= write_fifo_rdata;
                    write_fifo_read <= 1'b1;
                    read_enabled <= 1'b0;
                    if (first_write_op) begin
                        mem_bus.address <= starting_address;
                        first_write_op <= 1'b0;
                    end
                end else if ((read_port == PORT_MEM) && !read_fifo_full && read_enabled) begin
                    mem_bus.request <= 1'b1;
                    mem_bus.write <= 1'b0;
                end
            end

            if (mem_bus.ack) begin
                mem_bus.request <= 1'b0;
                mem_bus.address[16:0] <= mem_bus.address[16:0] + 2'd2;
            end

            if (end_op) begin
                read_enabled <= 1'b0;
                n64_scb.sram_done <= sram_selected && !first_write_op;
            end
        end
    end

    always_comb begin
        read_fifo_write = !mem_bus.write && mem_bus.ack;
        read_fifo_wdata = mem_bus.rdata;
        mem_bus.wmask = 2'b11;
    end


    // Reg bus controller

    always_ff @(posedge clk) begin
        if (aleh_op) begin
            reg_bus.address[16] <= n64_pi_dq_in[0];
        end

        if (alel_op) begin
            reg_bus.address[15:0] <= n64_pi_dq_in;
        end

        if (read_op || write_op) begin
            reg_bus.address <= reg_bus.address + 2'd2;
        end
    end

    always_comb begin
        reg_bus.read = read_op && (read_port == PORT_REG);
        reg_bus.write = write_op && (write_port == PORT_REG);
        reg_bus.wdata = n64_pi_dq_in;
    end

endmodule
