/**
 * PAT80 Emulator
 *
 * Emulates a PAT80.
 */

#include <Z/constants/pointer.h> /* Z_NULL */
#include <Z80.h>
#include <string.h>
#include <stdlib.h>
#include <ncurses.h>

#define ROM_SIZE         0x8000 /* 32 KiB */
#define MEMORY_SIZE      0xFFFF /* 64 KiB */
#define TERMINAL_WIDTH   60
#define TERMINAL_HEIGHT  25
#define SPACING_BETWEEN_WINDOWS 3
#define INSTRUCTION_WINDOW_HEIGHT 3

typedef struct {
        void* context;
        zuint8 (* read)(void *context);
        void (* write)(void *context, zuint8 value);
} Device;

typedef struct {
        zusize  cycles;
        zuint8  memory[65536];
        Z80     cpu;
        WINDOW  *terminal_win;
        WINDOW  *status_win;
} Machine;


static zuint8 machine_cpu_read(Machine *self, zuint16 address) {
        return address < MEMORY_SIZE ? self->memory[address] : 0xFF;
}


static void machine_cpu_write(Machine *self, zuint16 address, zuint8 value) {
        if (address >= ROM_SIZE && address < MEMORY_SIZE)
                self->memory[address] = value;
}


static zuint8 machine_cpu_in(Machine *self, zuint16 port) {
	// Pat80 has 8 devices, decoded based on the 3 most significant IO addr bits.
        // Note the Z80 has 16 bit address bus, but only the first 8 are used as IO addr,
        // so the 3 most significant IO addr bits in this case are A7, A6, A5. The bits
        // A4-A0 may be used by the single device, at its own discretion.
        zuint16 bitmask = 7; // 0000000000000111
        int decoded = port & bitmask;
	if (decoded <= 0x1F) {
		// Port 0 (0x00 to 0x1F): terminal
		// Read char from stin
                char c = getch();
                // Intercept emulator commands
                switch (c) {
                        case 27:
                                // ESC: shutdown emulator
                                exit(0);        // TODO: Shutdown ncurses and emulator cleanly
                                return 0;
                        default:
                                // Deliver keypress to pat80
                                return c;
                }
        }
        if (decoded <=  0x3F) {
                // Port 1 (0x20 to 0x3F): sound card (sn76489)
		wprintw(self->status_win, "sound_cmd[IN]: Not supported!\n");
                return 0x00;
        }
        if (decoded <=  0x5F) {
                // Port 2 (0x40 to 0x5F)
                wprintw(self->status_win, "IO_ERROR_IN: No device at port 2\n");
                return 0x00;
        }
        if (decoded <=  0x7F) {
                // Port 3 (0x60 to 0x7F)
                wprintw(self->status_win, "IO_ERROR_IN: No device at port 3\n");
                return 0x00;
        }
        if (decoded <=  0x9F) {
                // Port 4 (0x80 to 0x9F)
                wprintw(self->status_win, "IO_ERROR_IN: No device at port 4\n");
                return 0x00;
        }
        if (decoded <=  0x5F) {
                // Port 5 (0xA0 to 0xBF)
                wprintw(self->status_win, "IO_ERROR_IN: No device at port 5\n");
                return 0x00;
        }
        if (decoded <=  0x5F) {
                // Port 6 (0xC0 to 0xDF)
                wprintw(self->status_win, "IO_ERROR_IN: No device at port 6\n");
                return 0x00;
        }
        if (decoded <=  0x5F) {
                // Port 7 (0xE0 to 0xFF)
                wprintw(self->status_win, "IO_ERROR_IN: No device at port 7\n");
        } else {
                wprintw(self->status_win, "IO_ERROR_IN: Invalid port address: %#04x\n", port);
        }

        // TODO: place this in a refresh cycle between CPU instructions
        refresh();
        wrefresh(self->terminal_win);
        wrefresh(self->status_win);
}


static void machine_cpu_out(Machine *self, zuint16 port, zuint8 value) {
	// Pat80 has 8 devices, decoded based on the 3 most significant IO addr bits.
        // Note the Z80 has 16 bit address bus, but only the first 8 are used as IO addr,
        // so the 3 most significant IO addr bits in this case are A7, A6, A5. The bits
        // A4-A0 may be used by the single device, at its own discretion.
        zuint16 bitmask = 0xE0; // 0000000011100000
        int decoded = port & bitmask;
	if (decoded <= 0x1F) {
		// Port 0 (0x00 to 0x1F): terminal
		wprintw(self->terminal_win, "%c", value);
	} else if (decoded <= 0x3F) {
                // Port 1 (0x20 to 0x3F): sound card (sn76489)
		wprintw(self->status_win, "sound_cmd[%#04x]\n", value);
        } else if (decoded <= 0x5F) {
                // Port 2 (0x40 to 0x5F)
                wprintw(self->status_win, "IO_ERROR_OUT: No device at port 2\n");
        } else if (decoded <= 0x7F) {
                // Port 3 (0x60 to 0x7F)
                wprintw(self->status_win, "IO_ERROR_OUT: No device at port 3\n");
        } else if (decoded <= 0x9F) {
                // Port 4 (0x80 to 0x9F)
                wprintw(self->status_win, "IO_ERROR_OUT: No device at port 4\n");
        } else if (decoded <= 0x5F) {
                // Port 5 (0xA0 to 0xBF)
                wprintw(self->status_win, "IO_ERROR_OUT: No device at port 5\n");
        } else if (decoded <= 0x5F) {
                // Port 6 (0xC0 to 0xDF)
                wprintw(self->status_win, "IO_ERROR_OUT: No device at port 6\n");
        } else if (decoded <= 0x5F) {
                // Port 7 (0xE0 to 0xFF)
                wprintw(self->status_win, "IO_ERROR_OUT: No device at port 7\n");
        } else {
                wprintw(self->status_win, "IO_ERROR_OUT: Invalid port address: %#04x\n", port);
        }

        // TODO: place this in a refresh cycle between CPU instructions
        refresh();
        wrefresh(self->terminal_win);
        wrefresh(self->status_win);
}


void machine_initialize(Machine *self) {
        self->cpu.context      = self;
        self->cpu.fetch_opcode =
        self->cpu.fetch        =
        self->cpu.nop          =
        self->cpu.read         = (Z80Read )machine_cpu_read;
        self->cpu.write        = (Z80Write)machine_cpu_write;
        self->cpu.in           = (Z80Read )machine_cpu_in;
        self->cpu.out          = (Z80Write)machine_cpu_out;
        self->cpu.halt         = Z_NULL;
        self->cpu.nmia         = Z_NULL;
        self->cpu.inta         = Z_NULL;
        self->cpu.int_fetch    = Z_NULL;
        self->cpu.ld_i_a       = Z_NULL;
        self->cpu.ld_r_a       = Z_NULL;
        self->cpu.reti         = Z_NULL;
        self->cpu.retn         = Z_NULL;
        self->cpu.hook         = Z_NULL;
        self->cpu.illegal      = Z_NULL;
        self->cpu.options      = Z80_MODEL_ZILOG_NMOS;
}


void machine_power(Machine *self, zbool state) {
        if (state)
                {
                self->cycles = 0;
                memset(self->memory, 0, MEMORY_SIZE);
                }

        z80_power(&self->cpu, state);
}


void machine_reset(Machine *self) {
        z80_instant_reset(&self->cpu);
}

void machine_run(Machine *self) {
        z80_run(&self->cpu, Z80_MAXIMUM_CYCLES);
}

int main(int argc, char *argv[]) {
        // Parse arguments
        if (argc < 2) {
                printf("Usage: %s [romFile]\n", argv[0]);
                exit(0);
        }
        char* romFilePath = argv[1];


        // Init ncurses
	initscr();			// Start curses mode
	raw();				// Line buffering disabled (get character without waiting for ENTER key)
	keypad(stdscr, TRUE);		// We get F1, F2 etc..
	noecho();			// Don't echo() while we do getch
        start_color();                  // Use colors
        init_pair(1, COLOR_WHITE, COLOR_BLUE); // Terminal window color
        init_pair(2, COLOR_YELLOW, COLOR_BLACK); // Status window color
        int x,y;
        getmaxyx(stdscr, y,x);

        // Setup virtual Pat80 computer
        Z80 pat80Cpu = {};
        Machine pat80 = {
                /*zusize*/  .cycles = 0,
                /*Z80*/     .cpu = pat80Cpu,
                .terminal_win = newwin(TERMINAL_HEIGHT, TERMINAL_WIDTH, INSTRUCTION_WINDOW_HEIGHT, 0),
                .status_win = newwin(y, x - TERMINAL_WIDTH - SPACING_BETWEEN_WINDOWS, INSTRUCTION_WINDOW_HEIGHT, TERMINAL_WIDTH + SPACING_BETWEEN_WINDOWS)
        };

        wbkgd(pat80.terminal_win, COLOR_PAIR(1));       // Ncurses: set terminal window color
        wbkgd(pat80.status_win, COLOR_PAIR(2));
        scrollok(pat80.terminal_win, TRUE);             // Ncurses: Allow scrolling when reached end of window
        scrollok(pat80.status_win, TRUE);
        attron(A_BOLD);                                 // Print instructions
        printw("Emulator commands\n");
        attroff(A_BOLD);
        printw("ESC: Exit");

        machine_initialize(&pat80);
        machine_power(&pat80, Z_TRUE);

        // Load ROM into memory
        FILE *romFile;
        romFile = fopen(romFilePath,"rb");
        if (romFile == NULL) {
                printf("Unable to open rom file at %s", romFilePath);
                exit(1);
        }
        fread(&pat80.memory,ROM_SIZE,1,romFile); // load rom from file into memory, up >
        fclose(romFile);

        // Start emulated computer
        machine_reset(&pat80);
        machine_run(&pat80);

        // Stop ncurses
        delwin(pat80.terminal_win);
        delwin(pat80.status_win);
        endwin();
        return 0;
}
