//
//  Cartridge.h
//  Clock Signal
//
//  Created by Thomas Harte on 17/03/2017.
//  Copyright © 2017 Thomas Harte. All rights reserved.
//

#ifndef Atari2600_Cartridge_hpp
#define Atari2600_Cartridge_hpp

#include "../../../Processors/6502/6502.hpp"
#include "../Bus.hpp"

namespace Atari2600 {
namespace Cartridge {

class BusExtender: public CPU::MOS6502::BusHandler {
	public:
		BusExtender(uint8_t *rom_base, size_t rom_size) : rom_base_(rom_base), rom_size_(rom_size) {}

		void advance_cycles(int cycles) {}

	protected:
		uint8_t *rom_base_;
		size_t rom_size_;
};

template<class T> class Cartridge:
	public CPU::MOS6502::BusHandler,
	public Bus {

	public:
		Cartridge(const std::vector<uint8_t> &rom) :
			m6502_(*this),
			rom_(rom),
			bus_extender_(rom_.data(), rom.size()) {
			// The above works because bus_extender_ is declared after rom_ in the instance storage list;
			// consider doing something less fragile.
		}

		void run_for(const Cycles cycles)	{ m6502_.run_for(cycles);		}
		void set_reset_line(bool state)		{ m6502_.set_reset_line(state);	}

		// to satisfy CPU::MOS6502::Processor
		Cycles perform_bus_operation(CPU::MOS6502::BusOperation operation, uint16_t address, uint8_t *value) {
			uint8_t returnValue = 0xff;
			int cycles_run_for = 3;

			// this occurs as a feedback loop — the 2600 requests ready, then performs the cycles_run_for
			// leap to the end of ready only once ready is signalled — because on a 6502 ready doesn't take
			// effect until the next read; therefore it isn't safe to assume that signalling ready immediately
			// skips to the end of the line.
			if(operation == CPU::MOS6502::BusOperation::Ready)
				cycles_run_for = tia_->get_cycles_until_horizontal_blank(cycles_since_video_update_);

			cycles_since_speaker_update_ += Cycles(cycles_run_for);
			cycles_since_video_update_ += Cycles(cycles_run_for);
			cycles_since_6532_update_ += Cycles(cycles_run_for / 3);
			bus_extender_.advance_cycles(cycles_run_for / 3);

			if(operation != CPU::MOS6502::BusOperation::Ready) {
				// give the cartridge a chance to respond to the bus access
				bus_extender_.perform_bus_operation(operation, address, value);

				// check for a RIOT RAM access
				if((address&0x1280) == 0x80) {
					if(isReadOperation(operation)) {
						returnValue &= mos6532_.get_ram(address);
					} else {
						mos6532_.set_ram(address, *value);
					}
				}

				// check for a TIA access
				if(!(address&0x1080)) {
					if(isReadOperation(operation)) {
						const uint16_t decodedAddress = address & 0xf;
						switch(decodedAddress) {
							case 0x00:		// missile 0 / player collisions
							case 0x01:		// missile 1 / player collisions
							case 0x02:		// player 0 / playfield / ball collisions
							case 0x03:		// player 1 / playfield / ball collisions
							case 0x04:		// missile 0 / playfield / ball collisions
							case 0x05:		// missile 1 / playfield / ball collisions
							case 0x06:		// ball / playfield collisions
							case 0x07:		// player / player, missile / missile collisions
								returnValue &= tia_->get_collision_flags(decodedAddress);
							break;

							case 0x08:
							case 0x09:
							case 0x0a:
							case 0x0b:
								// TODO: pot ports
								returnValue &= 0;
							break;

							case 0x0c:
							case 0x0d:
								returnValue &= tia_input_value_[decodedAddress - 0x0c];
							break;
						}
					} else {
						const uint16_t decodedAddress = address & 0x3f;
						switch(decodedAddress) {
							case 0x00:	update_video(); tia_->set_sync(*value & 0x02);		break;
							case 0x01:	update_video();	tia_->set_blank(*value & 0x02);		break;

							case 0x02:	m6502_.set_ready_line(true);						break;
							case 0x03:	update_video();	tia_->reset_horizontal_counter();	break;
								// TODO: audio will now be out of synchronisation — fix

							case 0x04:
							case 0x05:	update_video();	tia_->set_player_number_and_size(decodedAddress - 0x04, *value);	break;
							case 0x06:
							case 0x07:	update_video();	tia_->set_player_missile_colour(decodedAddress - 0x06, *value);		break;
							case 0x08:	update_video();	tia_->set_playfield_ball_colour(*value);							break;
							case 0x09:	update_video();	tia_->set_background_colour(*value);								break;
							case 0x0a:	update_video();	tia_->set_playfield_control_and_ball_size(*value);					break;
							case 0x0b:
							case 0x0c:	update_video();	tia_->set_player_reflected(decodedAddress - 0x0b, !((*value)&8));	break;
							case 0x0d:
							case 0x0e:
							case 0x0f:	update_video();	tia_->set_playfield(decodedAddress - 0x0d, *value);					break;
							case 0x10:
							case 0x11:	update_video(); tia_->set_player_position(decodedAddress - 0x10);					break;
							case 0x12:
							case 0x13:	update_video(); tia_->set_missile_position(decodedAddress - 0x12);					break;
							case 0x14:	update_video();	tia_->set_ball_position();											break;
							case 0x1b:
							case 0x1c:	update_video(); tia_->set_player_graphic(decodedAddress - 0x1b, *value);			break;
							case 0x1d:
							case 0x1e:	update_video(); tia_->set_missile_enable(decodedAddress - 0x1d, (*value)&2);		break;
							case 0x1f:	update_video(); tia_->set_ball_enable((*value)&2);									break;
							case 0x20:
							case 0x21:	update_video(); tia_->set_player_motion(decodedAddress - 0x20, *value);				break;
							case 0x22:
							case 0x23:	update_video(); tia_->set_missile_motion(decodedAddress - 0x22, *value);			break;
							case 0x24:	update_video(); tia_->set_ball_motion(*value);										break;
							case 0x25:
							case 0x26:	tia_->set_player_delay(decodedAddress - 0x25, (*value)&1);							break;
							case 0x27:	tia_->set_ball_delay((*value)&1);													break;
							case 0x28:
							case 0x29:	update_video(); tia_->set_missile_position_to_player(decodedAddress - 0x28, (*value)&2);		break;
							case 0x2a:	update_video(); tia_->move();														break;
							case 0x2b:	update_video(); tia_->clear_motion();												break;
							case 0x2c:	update_video(); tia_->clear_collision_flags();										break;

							case 0x15:
							case 0x16:	update_audio(); speaker_->set_control(decodedAddress - 0x15, *value);				break;
							case 0x17:
							case 0x18:	update_audio(); speaker_->set_divider(decodedAddress - 0x17, *value);				break;
							case 0x19:
							case 0x1a:	update_audio(); speaker_->set_volume(decodedAddress - 0x19, *value);				break;
						}
					}
				}

				// check for a PIA access
				if((address&0x1280) == 0x280) {
					update_6532();
					if(isReadOperation(operation)) {
						returnValue &= mos6532_.get_register(address);
					} else {
						mos6532_.set_register(address, *value);
					}
				}

				if(isReadOperation(operation)) {
					*value &= returnValue;
				}
			}

			if(!tia_->get_cycles_until_horizontal_blank(cycles_since_video_update_)) m6502_.set_ready_line(false);

			return Cycles(cycles_run_for / 3);
		}

		void flush() {
			update_audio();
			update_video();
			speaker_->flush();
		}

	protected:
		CPU::MOS6502::Processor<Cartridge<T>, true> m6502_;
		std::vector<uint8_t> rom_;

	private:
		T bus_extender_;
};

}
}

#endif /* Atari2600_Cartridge_hpp */
