module cpu(input wire clk,
           input wire tick_60hz,
           input wire tick_next,
           input wire [15:0] keys,
           output wire out,
           input wire scr_busy,
           input wire scr_read,
           input wire [7:0] scr_read_idx,
           output reg [7:0] scr_read_byte,
           output reg scr_read_ack);
  assign out = st != 0;

  // Press 0 + F to activate debug mode
  wire debug_mode = (keys == 16'b1000_0000_0000_0010);

  // Memory map:
  // 000..01F: stack (16 x 2 bytes)
  // 020..02F: registers (16 x 1 byte)
  // 030..07F: font (16 x 5 bytes)
  // 100..1FF: screen (32 lines x 8 bytes)

  // Memory

  reg mem_read = 0;
  reg [11:0] mem_read_idx;
  wire [7:0] mem_read_byte;
  wire mem_read_ack;

  reg mem_write = 0;
  reg [11:0] mem_write_idx;
  reg [7:0] mem_write_byte;

  mem mem0(clk,
           mem_read, mem_read_idx, mem_read_byte, mem_read_ack,
           mem_write, mem_write_idx, mem_write_byte);

  localparam
    STATE_NEXT = 0,
    STATE_FETCH_HI = 1,
    STATE_FETCH_LO = 2,
    STATE_POP_HI = 3,
    STATE_POP_LO = 4,
    STATE_PUSH_HI = 5,
    STATE_PUSH_LO = 6,
    STATE_DECODE = 7,
    STATE_TRANSFER_LOAD = 8,
    STATE_TRANSFER_STORE = 9,
    STATE_CLEAR = 10,
    STATE_LOAD_VX = 11,
    STATE_LOAD_VY = 12,
    STATE_LOAD_V0 = 13,
    STATE_STORE_VX = 14,
    STATE_STORE_CARRY = 15,
    STATE_BCD_1 = 16,
    STATE_BCD_2 = 17,
    STATE_BCD_3 = 18,
    STATE_GPU = 19,
    // ...
    STATE_STOP = 31;

  reg[4:0] state = STATE_NEXT;

  // BCD
  wire [1:0] bcd_1;
  wire [3:0] bcd_2, bcd_3;
  bcd bcd0(vx, bcd_1, bcd_2, bcd_3);

  // GPU
  reg gpu_draw = 0;
  reg [11:0] gpu_addr;
  reg [3:0] gpu_lines;
  reg [5:0] gpu_x;
  reg [4:0] gpu_y;
  wire gpu_busy;
  wire gpu_collision;
  wire gpu_read;
  wire [11:0] gpu_read_idx;
  wire gpu_write;
  wire [11:0] gpu_write_idx;
  wire [7:0] gpu_write_byte;

  gpu gpu0(.clk(clk),
           .draw(gpu_draw),
           .addr(gpu_addr),
           .lines(gpu_lines),
           .x(gpu_x),
           .y(gpu_y),
           .busy(gpu_busy),
           .collision(gpu_collision),
           .mem_read(gpu_read),
           .mem_read_idx(gpu_read_idx),
           .mem_read_byte(mem_read_byte), // pass-through
           .mem_read_ack(mem_read_ack), // pass-through
           .mem_write(gpu_write),
           .mem_write_idx(gpu_write_idx),
           .mem_write_byte(gpu_write_byte));

  // Memory loads and stores
  always @(*) begin
    mem_read = 0;
    mem_write = 0;
    mem_read_idx = 0;

    case (state)
      STATE_NEXT, STATE_STOP: begin
        mem_read = scr_read;
        mem_read_idx = {4'h1, scr_read_idx};
        if (debug_mode) begin
          if (scr_read_idx < 'h30)
            // Show stack and registers
            mem_read_idx = {4'h0, scr_read_idx};
          else
            // Show beginning of program memory
            mem_read_idx = {4'h2, scr_read_idx - 8'h30};
        end
      end
      STATE_FETCH_HI: if (!mem_read_ack) begin
        mem_read = 1;
        mem_read_idx = pc[11:0];
      end
      STATE_FETCH_LO: if (!mem_read_ack) begin
        mem_read = 1;
        mem_read_idx = pc[11:0] + 1;
      end
      STATE_LOAD_VX: if (!mem_read_ack) begin
        mem_read = 1;
        mem_read_idx = 'h20 + {8'b0, x};
      end
      STATE_LOAD_VY: if (!mem_read_ack) begin
        mem_read = 1;
        mem_read_idx = 'h20 + {8'b0, y};
      end
      STATE_LOAD_V0: if (!mem_read_ack) begin
        mem_read = 1;
        mem_read_idx = 'h20;
      end
      STATE_POP_HI: if (!mem_read_ack) begin
        mem_read = 1;
        mem_read_idx = 2 * sp;
      end
      STATE_POP_LO: if (!mem_read_ack) begin
        mem_read = 1;
        mem_read_idx = 2 * sp + 1;
      end
      STATE_PUSH_HI: begin
        mem_write = 1;
        mem_write_idx = 2 * sp - 2;
        mem_write_byte = {4'b0, ret_pc[11:8]};
      end
      STATE_PUSH_LO: begin
        mem_write = 1;
        mem_write_idx = 2 * sp - 1;
        mem_write_byte = ret_pc[7:0];
      end
      STATE_TRANSFER_LOAD: if (!mem_read_ack) begin
        mem_read = 1;
        mem_read_idx = transfer_src_addr + {4'b0, transfer_counter};
      end
      STATE_TRANSFER_STORE: begin
        mem_write = 1;
        mem_write_idx = transfer_dest_addr + {4'b0, transfer_counter};
        mem_write_byte = mem_read_byte;
      end
      STATE_CLEAR: begin
        mem_write = 1;
        mem_write_idx = transfer_dest_addr + {4'b0, transfer_counter};
        mem_write_byte = 0;
      end
      STATE_STORE_VX: begin
        mem_write = 1;
        mem_write_idx = 'h20 + {8'b0, x};
        mem_write_byte = new_vx;
      end
      STATE_STORE_CARRY: begin
        mem_write = 1;
        mem_write_idx = 'h2F;
        mem_write_byte = {7'b0, carry};
      end
      STATE_BCD_1: begin
        mem_write = 1;
        mem_write_idx = addr;
        mem_write_byte = {6'b0, bcd_1};
      end
      STATE_BCD_2: begin
        mem_write = 1;
        mem_write_idx = addr + 1;
        mem_write_byte = {4'b0, bcd_2};
      end
      STATE_BCD_3: begin
        mem_write = 1;
        mem_write_idx = addr + 2;
        mem_write_byte = {4'b0, bcd_3};
      end
      STATE_GPU: begin
        mem_read = gpu_read;
        mem_read_idx = gpu_read_idx;
        mem_write = gpu_write;
        mem_write_idx = gpu_write_idx;
        mem_write_byte = gpu_write_byte;
      end
    endcase
  end

  // Registers
  reg [11:0] pc = 'h200;
  reg [11:0] ret_pc;
  reg [11:0] addr = 0;
  reg [11:0] transfer_src_addr, transfer_dest_addr;
  reg [7:0] transfer_counter;
  reg [3:0] sp = 0;
  reg [7:0] dt = 0;
  reg [7:0] st = 0;

  // Instruction
  reg [15:0] instr;
  wire [3:0] a = instr[15:12];
  wire [3:0] x = instr[11:8];
  wire [3:0] y = instr[7:4];
  wire [3:0] z = instr[3:0];
  wire [7:0] yz = instr[7:0];
  wire [11:0] xyz = instr[11:0];

  reg [7:0] vx, vy, new_vx;
  reg carry;
  wire needs_carry = a == 'h8 && (z == 'h4 || z == 'h5 || z == 'h6 || z == 'h7 || z == 'hE);

  // Can go to the next instruction (for rate limiting by tick_next)
  reg next = 1;

  wire [31:0] rng_state;
  rng rng(.clk(clk), .out(rng_state), .user_input(&keys));

  integer i;

  always @(posedge clk) begin
    if (tick_60hz) begin
      $display($time, " tick, dt = %x st = %x", dt, st);
      if (dt != 0)
        dt <= dt - 1;
      if (st != 0)
        st <= st - 1;
    end

    if (tick_next)
      next <= 1;

    scr_read_ack <= 0;

    case (state)
      STATE_NEXT, STATE_STOP: begin
        if (scr_read && mem_read_ack) begin
          scr_read_ack <= 1;
          scr_read_byte <= mem_read_byte;
        end
        if (state == STATE_NEXT && !scr_busy && next) begin
          next <= 0;
          state <= STATE_FETCH_HI;
        end
      end
      STATE_FETCH_HI:
        if (mem_read_ack) begin
          instr[15:8] <= mem_read_byte;
          state <= STATE_FETCH_LO;
        end
      STATE_FETCH_LO:
        if (mem_read_ack) begin
          instr[7:0] <= mem_read_byte;
          if (a == 'hB)
            // JP V0, xyz
            state <= STATE_LOAD_V0;
          else
            // TODO check if necessary?
            state <= STATE_LOAD_VX;
        end
      STATE_LOAD_VX:
        if (mem_read_ack) begin
          vx <= mem_read_byte;
          // TODO check if necessary?
          state <= STATE_LOAD_VY;
        end
      STATE_LOAD_VY:
        if (mem_read_ack) begin
          vy <= mem_read_byte;
          state <= STATE_DECODE;
        end
      STATE_LOAD_V0:
        if (mem_read_ack) begin
          vx <= mem_read_byte;
          state <= STATE_DECODE;
        end
      STATE_STORE_VX:
        state <= needs_carry ? STATE_STORE_CARRY : STATE_NEXT;
      STATE_STORE_CARRY:
        state <= STATE_NEXT;
      STATE_POP_HI:
        if (mem_read_ack) begin
          pc[11:8] <= mem_read_byte[3:0];
          state <= STATE_POP_LO;
        end
      STATE_POP_LO:
        if (mem_read_ack) begin
          pc[7:0] <= mem_read_byte;
          state <= STATE_NEXT;
        end
      STATE_PUSH_HI:
        state <= STATE_PUSH_LO;
      STATE_PUSH_LO:
        state <= STATE_NEXT;
      STATE_TRANSFER_LOAD:
        if (mem_read_ack)
          state <= STATE_TRANSFER_STORE;
      STATE_TRANSFER_STORE:
        if (transfer_counter == 0)
          state <= STATE_NEXT;
        else begin
          transfer_counter <= transfer_counter - 1;
          state <= STATE_TRANSFER_LOAD;
        end
      STATE_CLEAR:
        if (transfer_counter == 0)
          state <= STATE_NEXT;
        else begin
          transfer_counter <= transfer_counter - 1;
        end
      STATE_BCD_1:
        state <= STATE_BCD_2;
      STATE_BCD_2:
        state <= STATE_BCD_3;
      STATE_BCD_3:
        state <= STATE_NEXT;
      STATE_GPU:
        begin
          gpu_draw <= 0;
          if (!gpu_draw && !gpu_busy) begin
            carry <= gpu_collision;
            state <= STATE_STORE_CARRY;
          end
        end
      STATE_DECODE: begin
        $display($time, " run [%x] %x", pc, instr);
        pc <= pc + 2;
        state <= STATE_NEXT;

        case (a)
          4'h0:
            case (xyz)
              'h0E0: begin
                $display($time, " instr: CLS");
                transfer_dest_addr <= 'h100;
                transfer_counter <= 'hFF;
                state <= STATE_CLEAR;
              end
              'h0EE: begin
                $display($time, " instr: RET");
                sp <= sp - 1;
                state <= STATE_POP_HI;
              end
              'h0FD: begin
                $display($time, " instr: EXIT");
                state <= STATE_STOP;
              end
              default: begin
                $display($time, " instr: NOP");
              end
            endcase
          4'h1: begin
            $display($time, " instr: JP %x", xyz);
            pc <= xyz;
          end
          4'h2: begin
            $display($time, " instr: CALL %x", xyz);
            sp <= sp + 1;
            state <= STATE_PUSH_HI;
            ret_pc <= pc + 2;
            pc <= xyz;
          end
          4'h3: begin
            $display($time, " instr: SE V%x, %x", x, yz);
            if (vx == yz)
              pc <= pc + 4;
          end
          4'h4: begin
            $display($time, " instr: SNE V%x, %x", x, yz);
            if (vx != yz)
              pc <= pc + 4;
          end
          4'h5: begin
            $display($time, " instr: SE V%x, V%x", x, y);
            if (vx == vy)
              pc <= pc + 4;
          end
          4'h9: begin
            $display($time, " instr: SNE V%x, V%x", x, y);
            if (vx != vy)
              pc <= pc + 4;
          end
          4'h6: begin
            $display($time, " instr: LD V%x, %x", x, yz);
            new_vx <= yz;
            state <= STATE_STORE_VX;
          end
          4'h7: begin
            $display($time, " instr: ADD V%x, %x", x, yz);
            new_vx <= vx + yz;
            state <= STATE_STORE_VX;
          end
          4'h8: begin
            case (z)
              4'h0: begin
                $display($time, " instr: LD V%x, V%x", x, y);
                new_vx <= vy;
                state <= STATE_STORE_VX;
              end
              4'h1: begin
                $display($time, " instr: OR V%x, V%x", x, y);
                new_vx <= vx | vy;
                state <= STATE_STORE_VX;
              end
              4'h2: begin
                $display($time, " instr: AND V%x, V%x", x, y);
                new_vx <= vx & vy;
                state <= STATE_STORE_VX;
              end
              4'h3: begin
                $display($time, " instr: XOR V%x, V%x", x, y);
                new_vx <= vx ^ vy;
                state <= STATE_STORE_VX;
              end
              4'h4: begin
                $display($time, " instr: ADD V%x, V%x", x, y);
                new_vx <= vx + vy;
                carry <= ((vx + vy) >= 'h100) ? 1 : 0;
                state <= STATE_STORE_VX;
              end
              4'h5: begin
                $display($time, " instr: SUB V%x, V%x", x, y);
                new_vx <= vx - vy;
                carry <= (vx > vy) ? 1 : 0;
                state <= STATE_STORE_VX;
              end
              4'h6: begin
                $display($time, " instr: SHR V%x", x);
                new_vx <= vx >> 1;
                carry <= vx[0];
                state <= STATE_STORE_VX;
              end
              4'h7: begin
                $display($time, " instr: SUBN V%x, V%x", x, y);
                new_vx <= vy - vx;
                carry <= (vy > vx) ? 1 : 0;
                state <= STATE_STORE_VX;
              end
              4'hE: begin
                $display($time, " instr: SHL V%x", x);
                new_vx <= vx << 1;
                carry <= vx[7];
                state <= STATE_STORE_VX;
              end
              default: ;
            endcase
          end
          4'hA: begin
            $display($time, " instr: LD I, %x", xyz);
            addr <= xyz;
          end
          4'hB: begin
            $display($time, " instr: JP V0, %x", xyz);
            // STATE_LOAD_V0 loaded V0 into vx
            pc <= xyz + {4'b0, vx};
          end
          4'hC: begin
            $display($time, " instr: RND V%x, %x", x, yz);
            new_vx <= rng_state[15:8] & yz;
            state <= STATE_STORE_VX;
          end
          4'hD: begin
            $display($time, " instr: DRW V%x, V%x, %x", x, y, z);
            gpu_draw <= 1;
            gpu_addr <= addr;
            gpu_lines <= z;
            gpu_x <= vx[5:0];
            gpu_y <= vy[4:0];
            state <= STATE_GPU;
          end
          4'hE: begin
            case (yz)
              8'h9E: begin
                $display($time, " instr: SKP V%x", x);
                // In debug mode, assume no keys are pressed.
                if (!debug_mode && keys[vx[3:0]])
                  pc <= pc + 4;
              end
              8'hA1: begin
                $display($time, " instr: SKNP V%x", x);
                // In debug mode, assume no keys are pressed.
                if (debug_mode || !keys[vx[3:0]])
                  pc <= pc + 4;
              end
              default: ;
            endcase
          end
          4'hF: begin
            case (yz)
              8'h07: begin
                $display($time, " instr: LD V%x, DT", x);
                new_vx <= dt;
                state <= STATE_STORE_VX;
              end
              8'h0A: begin
                $display($time, " instr: LD V%x, K", x);
                if (keys == 0)
                  pc <= pc;
                else begin
                  for (i = 15; i >= 0; i--)
                    if (keys[i])
                      vx <= i[7:0];
                end
              end
              8'h15: begin
                $display($time, " instr: LD DT, V%x", x);
                dt <= vx;
              end
              8'h18: begin
                $display($time, " instr: LD ST, V%x", x);
                st <= vx;
              end
              8'h1E: begin
                $display($time, " instr: ADD I, V%x", x);
                addr <= addr + {4'b0, vx};
              end
              8'h29: begin
                $display($time, " instr: LD F, V%x", x);
                addr <= 'h30 + vx * 5;
              end
              8'h33: begin
                $display($time, " instr: LD B, V%x", x);
                state <= STATE_BCD_1;
              end
              8'h55: begin
                $display($time, " instr: LD [I], V%x", x);
                transfer_src_addr <= 'h020;
                transfer_dest_addr <= addr;
                transfer_counter <= {4'b0, x};
                state <= STATE_TRANSFER_LOAD;
              end
              8'h65: begin
                $display($time, " instr: LD V%x, [I]", x);
                transfer_src_addr <= addr;
                transfer_dest_addr <= 'h020;
                transfer_counter <= {4'b0, x};
                state <= STATE_TRANSFER_LOAD;
              end
              default: ;
            endcase
          end
        endcase
      end
    endcase
  end
endmodule