/*
 * Copyright (c) 2021-2022 Rupert Carmichael
 *
 * This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/.
 */

// Electrically Erasable Programmable Read-Only Memory

#include <stdio.h>
#include <stdint.h>

#include "cega_eeprom.h"
#include "cega_serial.h"

/* 93C46B Instruction Set
   ======================================================================
   | Instruction | SB | Opcode |  Address | Data In | Data Out | Cycles |
   ======================================================================
   | ERASE       |  1 | 11     |   A5-A0  | -       | RDY/BSY  | 9      |
   ----------------------------------------------------------------------
   | ERAL        |  1 | 00     | 10xxxxxx | -       | RDY/BSY  | 9      |
   ----------------------------------------------------------------------
   | EWDS        |  1 | 00     | 00xxxxxx | -       | HIGH-Z   | 9      |
   ----------------------------------------------------------------------
   | EWEN        |  1 | 00     | 11xxxxxx | -       | HIGH-Z   | 9      |
   ----------------------------------------------------------------------
   | READ        |  1 | 10     |   A5-A0  | -       | D15-D0   | 25     |
   ----------------------------------------------------------------------
   | WRITE       |  1 | 01     |   A5-A0  | D15-D0  | RDY/BSY  | 25     |
   ----------------------------------------------------------------------
   | WRAL        |  1 | 00     | 01xxxxxx | D15-D0  | RDY/BSY  | 25     |
   ----------------------------------------------------------------------
*/
enum opcodes_93c46 {
    EWDS  = 0x00, WRAL  = 0x10, ERAL  = 0x20, EWEN  = 0x30, // Extended Opcodes
    WRITE = 0x40, READ  = 0x80, ERASE = 0xc0 // Addressed Opcodes
};

static eep93c46_t eeprom;

void cega_eeprom_init_93c46(void) {
    eeprom.clk = 0;
    eeprom.cs = 0;
    eeprom.cyc = 0;
    eeprom.dataout = 0x01;
    eeprom.opcode = 0;
    eeprom.addr = 0x00;
    eeprom.mode = EEP93C46_MODE_START;
    eeprom.wrenable = 0;
    eeprom.datain = 0x0000;
}

uint8_t cega_eeprom_rd_93c46(void) {
    // Return the pin status of CS, CLK, and DO
    return eeprom.cs | 0x02 | eeprom.dataout; // CLK is always returned high
}

void cega_eeprom_wr_93c46(uint8_t *mem, uint8_t data) {
    /* CS high selects the device. CS low deselects the device and forces it
       into standby mode.
    */
    if(!(data & 0x04)) { // CS is low
        if (eeprom.cs) // CS is going high to low, wait for START bit
            eeprom.mode = EEP93C46_MODE_START;

        eeprom.cyc = 0;
        eeprom.cs = data & 0x04;
        eeprom.clk = data & 0x02;
        eeprom.dataout = 0x01;
        return;
    }

    // Check if CLK is going low to high, and record the CLK and CS pin states
    int clk_lowhigh = !eeprom.clk && (data & 0x02);
    eeprom.clk = data & 0x02;
    eeprom.cs = data & 0x04;

    // All operations are done when CLK goes from low to high
    if (clk_lowhigh) {
        /* The START bit is detected the first time CS and DI are both high
           with respect to the positive edge of CLK. This is the first of 9
           cycles, with the remaining 8 being the opcode and address. Another
           16 cycles will be used for some instructions.
        */
        if (eeprom.mode == EEP93C46_MODE_START && (data & 0x01)) {
            eeprom.opcode = 0; // Blank the opcode
            eeprom.mode = EEP93C46_MODE_OPCODE; // Opcode read
        }
        else if (eeprom.mode == EEP93C46_MODE_OPCODE) {
            /* 8 bits are shifted in serially to create the Opcode/Address.
               If this opcode does not use the address bits, they still must
               be clocked in.
            */
            eeprom.opcode = (eeprom.opcode << 1) | (data & 0x01);

            // Decode Opcode and Address after 8 bits have been shifted in
            if (++eeprom.cyc < 8)
                return;

            // Reset cycle counter
            eeprom.cyc = 0;

            /* Address field used for some opcodes -- since there are 0x80
               bytes, accessed two at a time (words), the address can be a
               maximum of 0x7e.
            */
            eeprom.addr = (eeprom.opcode & 0x3f) << 1;

            // Decode the opcode and set the mode for the next write
            switch (eeprom.opcode & 0xc0) {
                case 0: { // Extended Opcodes
                    switch (eeprom.opcode & 0x30) {
                        case EWDS: { // Erase/Write Disable
                            eeprom.wrenable = 0;
                            eeprom.mode = EEP93C46_MODE_STANDBY;
                            break;
                        }
                        case WRAL: { // Write All
                            eeprom.datain = 0x0000;
                            eeprom.mode = EEP93C46_MODE_WRAL;
                            break;
                        }
                        case ERAL: { // Erase All
                            if (eeprom.wrenable) {
                                for (int i = 0; i < 0x80; ++i)
                                    mem[i] = 0xff;
                            }
                            eeprom.mode = EEP93C46_MODE_STANDBY;
                            eeprom.dataout = 0x01; // BSY
                            break;
                        }
                        case EWEN: { // Erase/Write Enable
                            eeprom.wrenable = 1;
                            eeprom.mode = EEP93C46_MODE_STANDBY;
                            break;
                        }
                    }
                    break;
                }
                case WRITE: { // Write
                    eeprom.datain = 0x0000;
                    eeprom.mode = EEP93C46_MODE_WRITE;
                    break;
                }
                case READ: { // Read
                    eeprom.datain =
                        mem[eeprom.addr] | (mem[eeprom.addr + 1] << 8);
                    eeprom.mode = EEP93C46_MODE_READ;
                    eeprom.dataout = 0x00;
                    break;
                }
                case ERASE: { // Erase
                    if (eeprom.wrenable)
                        mem[eeprom.addr] = mem[eeprom.addr + 1] = 0xff;
                    eeprom.mode = EEP93C46_MODE_STANDBY;
                    eeprom.dataout = 0x01; // BSY
                    break;
                }
            }
        }
        else if (eeprom.mode == EEP93C46_MODE_WRAL) { // Write All
            // Shift 16 bits in and then fill the entire 64 words with the data
            eeprom.datain = (eeprom.datain << 1) | (data & 0x01);

            if (++eeprom.cyc < 16)
                return;

            if (eeprom.wrenable) {
                for (int i = 0; i < 0x80;) { // 128 bytes == 64 words
                    mem[i++] = eeprom.datain & 0xff;
                    mem[i++] = eeprom.datain >> 8;
                }
            }
            eeprom.mode = EEP93C46_MODE_STANDBY;
            eeprom.dataout = 0x01; // BSY
        }
        else if (eeprom.mode == EEP93C46_MODE_WRITE) { // Write
            // Shift 16 bits in and then write to an address
            eeprom.datain = (eeprom.datain << 1) | (data & 0x01);

            if (++eeprom.cyc < 16)
                return;

            if (eeprom.wrenable) {
                mem[eeprom.addr] = eeprom.datain & 0xff;
                mem[eeprom.addr + 1] = eeprom.datain >> 8;
            }
            eeprom.mode = EEP93C46_MODE_STANDBY;
            eeprom.dataout = 0x01; // BSY
        }
        else if (eeprom.mode == EEP93C46_MODE_READ) { // Read
            // Shift each of the 16 bits out serially
            eeprom.dataout = ((eeprom.datain >> (15 - eeprom.cyc)) & 0x01);

            if (++eeprom.cyc < 16)
                return;

            // Increment the address to read the next 16-bit value
            eeprom.addr = (eeprom.addr + 2) & 0x7e;
            eeprom.datain = mem[eeprom.addr] | (mem[eeprom.addr + 1] << 8);
        }
    }
}

void eeprom_state_load_93c46(uint8_t *st) {
    eeprom.clk = cega_serial_pop8(st);
    eeprom.cs = cega_serial_pop8(st);
    eeprom.cyc = cega_serial_pop8(st);
    eeprom.dataout = cega_serial_pop8(st);
    eeprom.opcode = cega_serial_pop8(st);
    eeprom.addr = cega_serial_pop8(st);
    eeprom.mode = cega_serial_pop8(st);
    eeprom.wrenable = cega_serial_pop8(st);
    eeprom.datain = cega_serial_pop16(st);
}

void eeprom_state_save_93c46(uint8_t *st) {
    cega_serial_push8(st, eeprom.clk);
    cega_serial_push8(st, eeprom.cs);
    cega_serial_push8(st, eeprom.cyc);
    cega_serial_push8(st, eeprom.dataout);
    cega_serial_push8(st, eeprom.opcode);
    cega_serial_push8(st, eeprom.addr);
    cega_serial_push8(st, eeprom.mode);
    cega_serial_push8(st, eeprom.wrenable);
    cega_serial_push16(st, eeprom.datain);
}
