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

#define CYCLES_PER_FRAME 69888
#define CYCLES_AT_INT    24
#define CYCLES_PER_INT   32
#define ROM_SIZE         0x8000 /* 32 KiB */
#define MEMORY_SIZE      0xFFFF /* 64 KiB */

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

typedef struct {
        zusize  cycles;
        zuint8  memory[65536];
        Z80     cpu;
        Device* devices;
        zusize  device_count;
} 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
		//printf("TERMINAL_READ");
                //return 'H';
                return 0x00;
        }
        if (decoded <=  0x3F) {
                // Port 1 (0x20 to 0x3F): sound card (sn76489)
		printf("\nsound_cmd[IN]: Not supported!\n");
                return 0x00;
        }
        if (decoded <=  0x5F) {
                // Port 2 (0x40 to 0x5F)
                printf("IO_ERROR_IN: No device at port 2\n");
                return 0x00;
        }
        if (decoded <=  0x7F) {
                // Port 3 (0x60 to 0x7F)
                printf("IO_ERROR_IN: No device at port 3\n");
                return 0x00;
        }
        if (decoded <=  0x9F) {
                // Port 4 (0x80 to 0x9F)
                printf("IO_ERROR_IN: No device at port 4\n");
                return 0x00;
        }
        if (decoded <=  0x5F) {
                // Port 5 (0xA0 to 0xBF)
                printf("IO_ERROR_IN: No device at port 5\n");
                return 0x00;
        }
        if (decoded <=  0x5F) {
                // Port 6 (0xC0 to 0xDF)
                printf("IO_ERROR_IN: No device at port 6\n");
                return 0x00;
        }
        if (decoded <=  0x5F) {
                // Port 7 (0xE0 to 0xFF)
                printf("IO_ERROR_IN: No device at port 7\n");
        } else {
                printf("IO_ERROR_IN: Invalid port address: %#04x\n", port);
        }
}


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
		printf("%c", value);
	} else if (decoded <= 0x3F) {
                // Port 1 (0x20 to 0x3F): sound card (sn76489)
		printf("\nsound_cmd[%#04x]\n", value);
        } else if (decoded <= 0x5F) {
                // Port 2 (0x40 to 0x5F)
                printf("IO_ERROR_OUT: No device at port 2\n");
        } else if (decoded <= 0x7F) {
                // Port 3 (0x60 to 0x7F)
                printf("IO_ERROR_OUT: No device at port 3\n");
        } else if (decoded <= 0x9F) {
                // Port 4 (0x80 to 0x9F)
                printf("IO_ERROR_OUT: No device at port 4\n");
        } else if (decoded <= 0x5F) {
                // Port 5 (0xA0 to 0xBF)
                printf("IO_ERROR_OUT: No device at port 5\n");
        } else if (decoded <= 0x5F) {
                // Port 6 (0xC0 to 0xDF)
                printf("IO_ERROR_OUT: No device at port 6\n");
        } else if (decoded <= 0x5F) {
                // Port 7 (0xE0 to 0xFF)
                printf("IO_ERROR_OUT: No device at port 7\n");
        } else {
                printf("IO_ERROR_OUT: Invalid port address: %#04x\n", port);
        }
}


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;

        /* Create and initialize devices... */
}


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);
}


// void machine_run_frame(Machine *self)
//         {
//         /* CPU cycles before the INT signal */
//         self->cycles += z80_execute(&self->cpu, CYCLES_AT_INT - self->cycles);

//         /* CPU cycles during the INT signal */
//         z80_int(&self->cpu, Z_TRUE);
//         self->cycles += z80_run(&self->cpu, (CYCLES_AT_INT + CYCLES_PER_INT) - self->cycles);
//         z80_int(&self->cpu, Z_FALSE);

//         /* CPU cycles after the INT signal */
//         self->cycles += z80_execute(&self->cpu, CYCLES_PER_FRAME - self->cycles);

//         self->cycles -= CYCLES_PER_FRAME;
//         }




// TEST IMPLEM DEVICE

zuint8 device_terminal_read(void *context) {
        return 'H';
        //return 0;
}

void device_terminal_write(void *context, zuint8 value) {
        printf("%c", value);
}

zuint8 device_sound_read(void *context) {
        return 0;
}

void device_sound_write(void *context, zuint8 value) {
        printf("sound[%c]\n", value);
}

// TEST IMPLEM DEVICE

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

        // Setup virtual Pat80 computer
        Z80 pat80Cpu = {};
        Machine pat80 = {
                /*zusize*/  .cycles = 100000,
                /*Z80*/     .cpu = pat80Cpu
        };

        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);

	// Declare system devices
	Device patoTerminal = {
                .read = *device_terminal_read,
                .write = *device_terminal_write,
        };
        Device patoSound = {
                .read = *device_sound_read,
                .write = *device_sound_write,
        };

	Device patoDevices[2] = {
                patoTerminal,   // Port 0 (0x00 to 0x1F)
                patoSound       // Port 1 (0x20 to 0x3F)
        };
	pat80.devices = patoDevices;
        pat80.device_count = 2;

        machine_reset(&pat80);
        machine_run(&pat80);
}
