/*
 * Copyright 2003-2015 (C) Raster Software Vigo (Sergio Costas)
 * This file is part of FBZX
 *
 * FBZX is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 3 of the License, or
 * (at your option) any later version.
 *
 * FBZX is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 *
 */

#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <signal.h>
#include <sys/wait.h>
#include <sstream>

#include "emulator.hh"
#include "llscreen.hh"
#include "menus.hh"
#include "microdrive.hh"
#include "spk_ay.hh"
#include "tape.hh"
#include "computer.hh"
#include "keyboard.hh"
#include "mouse.hh"

#include "llsound.hh"
#include "z80free/Z80free.h"

class computer *ordenador;

computer::computer() {
	this->dblscan = false;
	this->bw      = false;

	this->page48k      = 0;
	this->bus_counter  = 0;
	this->port254      = 0;
	this->issue_3      = true;
	this->current_mode = MODE_48K;
	this->turbo        = false;
	this->turbo_play   = true;

	this->tape_write     = false;
	this->tape_fast_load = false; // don't fast load by default

	this->other_ret = 0;

	this->sound_bit = 0;

	this->memcontended_zone = 0;

	this->cicles_counter = 0;

	this->interr = 0;

	OOTape->register_signal("pause_tape", this);
	OOTape->register_signal("pause_tape_48k", this);
}

computer::~computer() {
}

bool computer::callback_receiver(string signal_received, class Signals *object) {
	if (signal_received == "pause_tape") {
		OOTape->set_pause(true);
		llsound->set_speed(ordenador->turbo); // set speed to the desired mode
		return true;
	}
	if (signal_received == "pause_tape_48k") {
		if ((this->current_mode == MODE_48K) || ((this->mport1 & 0x20) != 0)) {
			OOTape->set_pause(true);
			llsound->set_speed(ordenador->turbo); // set speed to the desired mode
		}
		return true;
	}
	return true;
}

/* Returns the bus value when reading a port without a periferial */

uint8_t computer::bus_empty() {
	if (ordenador->current_mode != MODE_P3) {
		return (screen->get_bus_value(this->cicles_counter));
	} else {
		return (255); // +2A and +3 returns always 255
	}
}

/* calls all the routines that emulates the computer, runing them for 'tstados'
 * tstates */

void computer::emulate(int tstados) {
	screen->show_screen(tstados);
	spk_ay->play_ay(tstados);
	spk_ay->play_sound(tstados);
	OOTape->play(tstados);
	if (OOTape->rewinded) {
		OOTape->set_pause(true);
		llsound->set_speed(ordenador->turbo);
	}
	microdrive->emulate(tstados);
	mouse->emulate(tstados);

	if (!OOTape->get_pause()) {
		if (OOTape->read_signal() != 0) {
			this->sound_bit = 1;
		} else {
			this->sound_bit = 0; // if not paused, asign SOUND_BIT the value of tape
		}
	}
}

// check if there's contention and waits the right number of tstates

void computer::do_contention(bool io, uint16_t addr) {
	if (this->memcontended_zone == 0) {
		return;
	}
	if (!io) {
		switch (ordenador->current_mode) {
		case MODE_128K:
		case MODE_P2:
		case MODE_128K_SPA:
			if (((this->mport1 & 0x01) == 1) && (addr >= 0xC000)) { // pages 1, 3, 5 and 7 and access to upper 16K block
				this->emulate(this->memcontended_zone); // are contended
				return;
			}
			break;

		case MODE_P3:
			if (this->mport2 & 0x01) {
				if ((this->mport2 & 0x06) == 0x00) { // 0, 1, 2 and 3, which are not contended
					return;
				}
				if ((this->mport2 & 0x06) == 0x02) { // 4, 5, 6 and 7, which are all contended
					this->emulate(this->memcontended_zone);
					return;
				}
				// here it can be 4, 5, 6 and 3, or 4, 7, 6 and 3. In both cases, there is contention only in the first 48K
				if (addr < 0xC000) {
					this->emulate(this->memcontended_zone); // are contended
				}
				return;
			}
			if (((this->mport1 & 0x04) == 4) && (addr >= 0xC000)) { // pages 4, 5, 6 and 7 and access to upper 16K block
				this->emulate(this->memcontended_zone); // are contended
				return;
			}
			break;

		default:
			break;
		}
	}
	if ((addr & 0xC000) == 0x4000) {
		this->emulate(this->memcontended_zone);
		return;
	}
	if (((addr & 0x0001) == 0) && io) {
		this->emulate(this->memcontended_zone);
		return;
	}
}

// resets the computer and loads the right ROMs

void ResetComputer() {
	Z80free_reset(&procesador);
	load_rom(ordenador->current_mode);

	ordenador->updown    = 0;
	ordenador->leftright = 0;

	ordenador->mport1       = 0;
	ordenador->mport2       = 0;
	ordenador->video_offset = 0;                         // video in page 9 (page 5 in 128K)
	switch (ordenador->current_mode) {
	case MODE_48K:                                       // 48K
		ordenador->block0 = ordenador->memoria;
		ordenador->block1 = ordenador->memoria + 131072; // video mem. in page 9 (page 5 in 128K)
		ordenador->block2 = ordenador->memoria + 65536;  // 2nd block in page 6 (page 2 in 128K)
		ordenador->block3 = ordenador->memoria + 65536;  // 3rd block in page 7 (page 3 in 128K)
		ordenador->mport1 = 32;                          // access to port 7FFD disabled
		screen->set_memory_pointers();
		break;

	case MODE_P3:               // +2A/+3
		Z80free_Out(0x1FFD, 0); // Here must NOT be a break, because +2A and +3 need both OUT, at 1FFD and 7FFD

	case MODE_128K:             // 128K
	case MODE_P2:               // +2
	case MODE_128K_SPA:         // spanish 128K
		Z80free_Out(0x7FFD, 0); // set the page for 128K (both spanish and british), +2, +2A and +3
		break;
	}
	spk_ay->reset();
	keyboard->reset();
	screen->reset(ordenador->current_mode);
	microdrive->reset();
	mouse->reset();
	OOTape->rewind();
}

void extra_contention() {
	ordenador->emulate(procesador.subtstates);
	ordenador->contended_cicles += procesador.subtstates;
	procesador.subtstates        = 0;
}

void Z80free_Wr(uint16_t Addr, uint8_t Value) {
	extra_contention();
	ordenador->do_contention(false, Addr);
	ordenador->write_memory(Addr, Value);
}

void computer::write_memory(uint16_t Addr, uint8_t Value) {
	switch (Addr & 0xC000) {
	case 0x0000:
		// only writes in the first 16K if we are in +3 mode and bit0 of mport2 is 1

		if ((ordenador->current_mode == MODE_P3) && (1 == (ordenador->mport2 & 0x01))) {
			*(ordenador->block0 + Addr) = (unsigned char) Value;
		}
		break;

	case 0x4000:
		*(ordenador->block1 + Addr) = (unsigned char) Value;
		break;

	case 0x8000:
		*(ordenador->block2 + Addr) = (unsigned char) Value;
		break;

	case 0xC000:
		*(ordenador->block3 + Addr) = (unsigned char) Value;
		break;
	}
}

uint8_t Z80free_Rd(uint16_t Addr) {
	extra_contention();

	if ((microdrive->mdr_active) && (microdrive->mdr_paged) && (Addr < 8192)) { // Interface I
		return ((uint8_t) ordenador->shadowrom[Addr]);
	}

	if (ordenador->other_ret == 1) {
		ordenador->other_ret = 0;
		return (201); // RET instruction
	}

	ordenador->do_contention(false, Addr);
	return (ordenador->read_memory(Addr));
}

uint8_t computer::read_memory(uint16_t Addr) {
	switch (Addr & 0xC000) {
	case 0x0000:
		return ((uint8_t) (*(ordenador->block0 + Addr)));

		break;

	case 0x4000:
		return ((uint8_t) (*(ordenador->block1 + Addr)));

		break;

	case 0x8000:
		return ((uint8_t) (*(ordenador->block2 + Addr)));

		break;

	case 0xC000:
		return ((uint8_t) (*(ordenador->block3 + Addr)));

		break;

	default:
		printf("Memory error\n");
		exit(1);
		return 0;
	}
}

void Z80free_Out(uint16_t Port, uint8_t Value) {

	if (((Port &0x00FF) == 254) && ((Value&0x80) != 0x00)) {
		if (ordenador->fps_counter != 0) {
			ordenador->fps = 50.0/ordenador->fps_counter;
			ordenador->fps_counter = 0;
		} else {
			ordenador->fps = 0;
		}
	}

	// Microdrive access

	uint16_t maskport;
	extra_contention();

	if (ordenador->current_mode != MODE_P3) {
		ordenador->do_contention(true, Port);
	}

	// ULAPlus
	if (Port == 0xBF3B) {
		screen->set_ulaplus_register(Value);
		return;
	}
	if (Port == 0xFF3B) {
		screen->set_ulaplus_value(Value);
	}

	if (((Port & 0x0018) != 0x0018) && (microdrive->mdr_active)) {
		microdrive->out(Port, Value);
	}

	// ULA port (A0 low)

	if (!(Port & 0x0001)) {
		ordenador->port254 = (unsigned char) Value;
		screen->border     = ((((unsigned char) Value) & 0x07));

		if (OOTape->get_pause()) {
			if (Value & 0x10) {
				ordenador->sound_bit = 1;
			} else {
				ordenador->sound_bit = 0; // assign to SOUND_BIT the value
			}
			if (Value & 0x08) {
				ordenador->mic_bit = 1;
			} else {
				ordenador->mic_bit = 1;
			}
		}
	}

	// Memory page (7FFD & 1FFD)

	if (ordenador->current_mode == MODE_P3) {
		maskport = 0x0FFD;
	} else {
		maskport = 0x3FFD;
	}

	if (((Port | maskport) == 0x7FFD) && (0 == (ordenador->mport1 & 0x20))) {
		ordenador->mport1 = (unsigned char) Value;
		screen->set_memory_pointers(); // set the pointers
	}

	if (((Port | maskport) == 0x1FFD) && (0 == (ordenador->mport1 & 0x20))) {
		ordenador->mport2 = (unsigned char) Value;
		screen->set_memory_pointers(); // set the pointers
	}

	// Sound chip (AY-3-8912)

	if (((Port | maskport) == 0xFFFD) && (spk_ay->ay_emul)) {
		spk_ay->set_latch(Value & 0x0F);
	}

	if (((Port | maskport) == 0xBFFD) && (spk_ay->ay_emul)) {
		spk_ay->set_value(Value);
	}
}

uint8_t Z80free_In(uint16_t Port) {
	static unsigned int temporal_io;
	uint8_t             pines;

	extra_contention();
	if (ordenador->current_mode != MODE_P3) {
		ordenador->do_contention(true, Port);
	}

	temporal_io = (unsigned int) Port;

	if (Port == 0xFF3B) {
		return (screen->read_ulaplus_value());
	}

	if (!(temporal_io & 0x0001)) {
		pines = 0xBF; // by default, sound bit is 0
		if (!(temporal_io & 0x0100)) {
			pines &= keyboard->s8;
		}
		if (!(temporal_io & 0x0200)) {
			pines &= keyboard->s9;
		}
		if (!(temporal_io & 0x0400)) {
			pines &= keyboard->s10;
		}
		if (!(temporal_io & 0x0800)) {
			pines &= keyboard->s11;
		}
		if (!(temporal_io & 0x1000)) {
			pines &= keyboard->s12;
		}
		if (!(temporal_io & 0x2000)) {
			pines &= keyboard->s13;
		}
		if (!(temporal_io & 0x4000)) {
			pines &= keyboard->s14;
		}
		if (!(temporal_io & 0x8000)) {
			pines &= keyboard->s15;
		}

		if (OOTape->get_pause()) {
			if (!ordenador->issue_3) {
				if (ordenador->port254 & 0x18) {
					pines |= 0x40;
				}
			} else {
				if (ordenador->port254 & 0x10) {
					pines |= 0x40;
				}
			}
			if (random() < (RAND_MAX / 200)) { // add tape noise when paused
				pines |= 0x40;
			}
		} else {
			if (OOTape->read_signal() != 0) {
				pines |= 0x40; // sound input
			} else {
				pines &= 0xBF; // sound input
			}
		}
		return (pines);
	}

	// Joystick
	if (!(temporal_io & 0x0020)) {
		if (keyboard->joystick == 1) {
			return (keyboard->js);
		} else {
			if (((Port & 0xFF00) == 0xFA00) && (mouse->enabled)) {
				return (mouse->button);
			}
			if (((Port & 0xFF00) == 0xFB00) && (mouse->enabled)) {
				return (mouse->x);
			}
			if (((Port & 0xFF00) == 0xFF00) && (mouse->enabled)) {
				return (mouse->y);
			}
			return 0; // if Kempston is not selected, emulate it, but always 0
		}
	}

	if ((temporal_io == 0xFFFD) && (spk_ay->ay_emul)) {
		return (spk_ay->get_value());
	}

	// Microdrive access

	if (((Port & 0x0018) != 0x0018) && (microdrive->mdr_active)) {
		return (microdrive->in(Port));
	}

	pines = ordenador->bus_empty();

	return (pines);
}
