//
//  9918Base.hpp
//  Clock Signal
//
//  Created by Thomas Harte on 14/12/2017.
//  Copyright 2017 Thomas Harte. All rights reserved.
//

#ifndef TMS9918Base_hpp
#define TMS9918Base_hpp

#include "ClockConverter.hpp"

#include "../../../ClockReceiver/ClockReceiver.hpp"
#include "../../../Numeric/BitReverse.hpp"
#include "../../../Outputs/CRT/CRT.hpp"

#include "AccessEnums.hpp"
#include "LineBuffer.hpp"
#include "PersonalityTraits.hpp"
#include "Storage.hpp"
#include "YamahaCommands.hpp"

#include <array>
#include <cassert>
#include <cstdint>
#include <cstring>
#include <memory>
#include <vector>

namespace TI::TMS {

constexpr uint8_t StatusInterrupt = 0x80;
constexpr uint8_t StatusSpriteOverflow = 0x40;

constexpr int StatusSpriteCollisionShift = 5;
constexpr uint8_t StatusSpriteCollision = 0x20;

template <Personality personality> struct Base: public Storage<personality> {
	Base();

	static constexpr int output_lag = 11;	// i.e. pixel output will occur 11 cycles
											// after corresponding data read.

	static constexpr uint32_t palette_pack(uint8_t r, uint8_t g, uint8_t b) {
		#if TARGET_RT_BIG_ENDIAN
			return uint32_t((r << 24) | (g << 16) | (b << 8));
		#else
			return uint32_t((b << 16) | (g << 8) | r);
		#endif
	}

	// The default TMS palette.
	static constexpr std::array<uint32_t, 16> default_palette {
		palette_pack(0, 0, 0),
		palette_pack(0, 0, 0),
		palette_pack(33, 200, 66),
		palette_pack(94, 220, 120),

		palette_pack(84, 85, 237),
		palette_pack(125, 118, 252),
		palette_pack(212, 82, 77),
		palette_pack(66, 235, 245),

		palette_pack(252, 85, 84),
		palette_pack(255, 121, 120),
		palette_pack(212, 193, 84),
		palette_pack(230, 206, 128),

		palette_pack(33, 176, 59),
		palette_pack(201, 91, 186),
		palette_pack(204, 204, 204),
		palette_pack(255, 255, 255)
	};
	const std::array<uint32_t, 16> &palette() {
		if constexpr (is_yamaha_vdp(personality)) {
			return Storage<personality>::solid_background_ ? Storage<personality>::palette_ : Storage<personality>::background_palette_;
		}
		return default_palette;
	}

	Outputs::CRT::CRT crt_;
	TVStandard tv_standard_ = TVStandard::NTSC;
	using AddressT = typename Storage<personality>::AddressT;

	/// Mutates @c target such that @c source replaces the @c length bits that currently start
	/// at bit @c shift . Subsequently ensures @c target is constrained by the
	/// applicable @c memory_mask.
	template <int shift, int length = 8> void install_field(AddressT &target, uint8_t source) {
		static_assert(length > 0 && length <= 8);
		constexpr auto source_mask = (1 << length) - 1;
		constexpr auto mask = AddressT(~(source_mask << shift));
		target = (
			(target & mask) |
			AddressT((source & source_mask) << shift)
		) & memory_mask(personality);
	}

	// Personality-specific metrics and converters.
	ClockConverter<personality> clock_converter_;

	// This VDP's DRAM.
	std::array<uint8_t, memory_size(personality)> ram_;

	// State of the DRAM/CRAM-access mechanism.
	AddressT ram_pointer_ = 0;
	uint8_t read_ahead_buffer_ = 0;
	MemoryAccess queued_access_ = MemoryAccess::None;
	int minimum_access_column_ = 0;

	// The main status register.
	uint8_t status_ = 0;

	// Current state of programmer input.
	bool write_phase_ = false;	// Determines whether the VDP is expecting the low or high byte of a write.
	uint8_t low_write_ = 0;		// Buffers the low byte of a write.

	// Various programmable flags.
	bool mode1_enable_ = false;
	bool mode2_enable_ = false;
	bool mode3_enable_ = false;
	bool blank_display_ = false;
	bool sprites_16x16_ = false;
	bool sprites_magnified_ = false;
	bool generate_interrupts_ = false;
	uint8_t sprite_height_ = 8;

	// Programmer-specified addresses.
	//
	// The TMS and descendants combine various parts of the address with AND operations,
	// e.g. the fourth byte in the pattern name table will be at `pattern_name_address_ & 4`;
	// ordinarily the difference between that and plain substitution is invisible because
	// the programmer mostly can't set low-enough-order bits. That's not universally true
	// though, so this implementation uses AND throughout.
	//
	// ... therefore, all programmer-specified addresses are seeded as all '1's. As and when
	// actual addresses are specified, the relevant bits will be substituted in.
	//
	// Cf. install_field.
	AddressT pattern_name_address_ = memory_mask(personality);				// Address of the tile map.
	AddressT colour_table_address_ = memory_mask(personality);				// Address of the colour map (if applicable).
	AddressT pattern_generator_table_address_ = memory_mask(personality);	// Address of the tile contents.
	AddressT sprite_attribute_table_address_ = memory_mask(personality);	// Address of the sprite list.
	AddressT sprite_generator_table_address_ = memory_mask(personality);	// Address of the sprite contents.

	// Default colours.
	uint8_t text_colour_ = 0;
	uint8_t background_colour_ = 0;

	// Internal mechanisms for position tracking.
	int latched_column_ = 0;

	// A struct to contain timing information that is a function of the current mode.
	struct {
		/*
			Vertical layout:

			Lines 0 to [pixel_lines]: standard data fetch and drawing will occur.
			... to [first_vsync_line]: refresh fetches will occur and border will be output.
			.. to [2.5 or 3 lines later]: vertical sync is output.
			... to [total lines - 1]: refresh fetches will occur and border will be output.
			... for one line: standard data fetch will occur, without drawing.
		*/
		int total_lines = 262;
		int pixel_lines = 192;
		int first_vsync_line = 227;

		// Maximum number of sprite slots to populate;
		// if sprites beyond this number should be visible
		// then the appropriate status information will be set.
		int maximum_visible_sprites = 4;

		// Set the position, in cycles, of the two interrupts,
		// within a line.
		//
		// TODO: redetermine where this number came from.
		struct {
			int column = 313;
			int row = 192;
		} end_of_frame_interrupt_position;
		int line_interrupt_position = -1;

		// Enables or disabled the recognition of the sprite
		// list terminator, and sets the terminator value.
		bool allow_sprite_terminator = true;
		uint8_t sprite_terminator(ScreenMode mode) {
			switch(mode) {
				default:	return 0xd0;
				case ScreenMode::YamahaGraphics3:
				case ScreenMode::YamahaGraphics4:
				case ScreenMode::YamahaGraphics5:
				case ScreenMode::YamahaGraphics6:
				case ScreenMode::YamahaGraphics7:
					return 0xd8;
			}
		}
	} mode_timing_;

	uint8_t line_interrupt_target_ = 0xff;
	uint8_t line_interrupt_counter_ = 0;
	bool enable_line_interrupts_ = false;
	bool line_interrupt_pending_ = false;
	bool vertical_active_ = false;

	ScreenMode screen_mode_, underlying_mode_;

	using LineBufferArray = std::array<LineBuffer, 313>;
	LineBufferArray line_buffers_;
	LineBufferArray::iterator fetch_line_buffer_;
	LineBufferArray::iterator draw_line_buffer_;
	void advance(LineBufferArray::iterator &iterator) {
		++iterator;
		if(iterator == line_buffers_.end()) {
			iterator = line_buffers_.begin();
		}
	}

	using SpriteBufferArray = std::array<SpriteBuffer, 313>;
	SpriteBufferArray sprite_buffers_;
	SpriteBufferArray::iterator fetch_sprite_buffer_;
	SpriteBuffer *fetched_sprites_ = nullptr;
	void advance(SpriteBufferArray::iterator &iterator) {
		++iterator;
		if(iterator == sprite_buffers_.end()) {
			iterator = sprite_buffers_.begin();
		}
	}
	void regress(SpriteBufferArray::iterator &iterator) {
		if(iterator == sprite_buffers_.begin()) {
			iterator = sprite_buffers_.end();
		}
		--iterator;
	}

	AddressT tile_offset_ = 0;
	uint8_t name_[4]{};
	void posit_sprite(int sprite_number, int sprite_y, uint8_t screen_row);

	// There is a delay between reading into the line buffer and outputting from there to the screen. That delay
	// is observeable because reading time affects availability of memory accesses and therefore time in which
	// to update sprites and tiles, but writing time affects when the palette is used and when the collision flag
	// may end up being set. So the two processes are slightly decoupled. The end of reading one line may overlap
	// with the beginning of writing the next, hence the two separate line buffers.
	LineBufferPointer output_pointer_, fetch_pointer_;

	int fetch_line() const;
	bool is_horizontal_blank() const;
	VerticalState vertical_state() const;

	int masked_address(int address) const;
	void write_vram(uint8_t);
	void write_register(uint8_t);
	void write_palette(uint8_t);
	void write_register_indirect(uint8_t);
	uint8_t read_vram();
	uint8_t read_register();

	void commit_register(int reg, uint8_t value);

	template <bool check_blank> ScreenMode current_screen_mode() const {
		if(check_blank && blank_display_) {
			return ScreenMode::Blank;
		}

		if constexpr (is_sega_vdp(personality)) {
			if(Storage<personality>::mode4_enable_) {
				return ScreenMode::SMSMode4;
			}
		}

		if constexpr (is_yamaha_vdp(personality)) {
			switch(Storage<personality>::mode_) {
				case 0b00001:	return ScreenMode::Text;
				case 0b01001:	return ScreenMode::YamahaText80;
				case 0b00010:	return ScreenMode::MultiColour;
				case 0b00000:	return ScreenMode::YamahaGraphics1;
				case 0b00100:	return ScreenMode::YamahaGraphics2;
				case 0b01000:	return ScreenMode::YamahaGraphics3;
				case 0b01100:	return ScreenMode::YamahaGraphics4;
				case 0b10000:	return ScreenMode::YamahaGraphics5;
				case 0b10100:	return ScreenMode::YamahaGraphics6;
				case 0b11100:	return ScreenMode::YamahaGraphics7;
			}
		}

		if(!mode1_enable_ && !mode2_enable_ && !mode3_enable_) {
			return ScreenMode::ColouredText;
		}

		if(mode1_enable_ && !mode2_enable_ && !mode3_enable_) {
			return ScreenMode::Text;
		}

		if(!mode1_enable_ && mode2_enable_ && !mode3_enable_) {
			return ScreenMode::Graphics;
		}

		if(!mode1_enable_ && !mode2_enable_ && mode3_enable_) {
			return ScreenMode::MultiColour;
		}

		// TODO: undocumented TMS modes.
		return ScreenMode::Blank;
	}

	static AddressT rotate(AddressT address) {
		return AddressT((address >> 1) | (address << 16)) & memory_mask(personality);
	}

	AddressT command_address(Vector location, bool expansion) const {
		if constexpr (is_yamaha_vdp(personality)) {
			switch(this->underlying_mode_) {
				default:
				case ScreenMode::YamahaGraphics4:	// 256 pixels @ 4bpp
					return AddressT(
						((location.v[0] >> 1) & 127) +
						(location.v[1] << 7)
					);

				case ScreenMode::YamahaGraphics5:	// 512 pixels @ 2bpp
					return AddressT(
						((location.v[0] >> 2) & 127) +
						(location.v[1] << 7)
					);

				case ScreenMode::YamahaGraphics6: {	// 512 pixels @ 4bpp
					const auto linear_address =
						AddressT(
							((location.v[0] >> 1) & 255) +
							(location.v[1] << 8)
						);
					return expansion ? linear_address : rotate(linear_address);
				}

				case ScreenMode::YamahaGraphics7: {	// 256 pixels @ 8bpp
					const auto linear_address =
						AddressT(
							((location.v[0] >> 0) & 255) +
							(location.v[1] << 8)
						);
					return expansion ? linear_address : rotate(linear_address);
				}
			}
		} else {
			return 0;
		}
	}

	uint8_t extract_colour(uint8_t byte, Vector location) const {
		switch(this->screen_mode_) {
			default:
			case ScreenMode::YamahaGraphics4:	// 256 pixels @ 4bpp
			case ScreenMode::YamahaGraphics6:	// 512 pixels @ 4bpp
				return (byte >> (((location.v[0] & 1) ^ 1) << 2)) & 0xf;

			case ScreenMode::YamahaGraphics5:	// 512 pixels @ 2bpp
				return (byte >> (((location.v[0] & 3) ^ 3) << 1)) & 0x3;

			case ScreenMode::YamahaGraphics7:	// 256 pixels @ 8bpp
				return byte;
		}
	}

	std::pair<uint8_t, uint8_t> command_colour_mask(Vector location) const {
		if constexpr (is_yamaha_vdp(personality)) {
			auto &context = Storage<personality>::command_context_;
			auto colour = context.latched_colour.has_value() ? context.latched_colour : context.colour;

			switch(this->screen_mode_) {
				default:
				case ScreenMode::YamahaGraphics4:	// 256 pixels @ 4bpp
				case ScreenMode::YamahaGraphics6:	// 512 pixels @ 4bpp
					return
						std::make_pair(
							0xf0 >> ((location.v[0] & 1) << 2),
							colour.colour4bpp
						);

				case ScreenMode::YamahaGraphics5:	// 512 pixels @ 2bpp
					return
						std::make_pair(
							0xc0 >> ((location.v[0] & 3) << 1),
							colour.colour2bpp
						);

				case ScreenMode::YamahaGraphics7:	// 256 pixels @ 8bpp
					return
						std::make_pair(
							0xff,
							colour.colour
						);
			}
		} else {
			return std::make_pair(0, 0);
		}
	}

	void do_external_slot(int access_column) {
		// Don't do anything if the required time for the access to become executable
		// has yet to pass.
		if(queued_access_ == MemoryAccess::None || access_column < minimum_access_column_) {
			if constexpr (is_yamaha_vdp(personality)) {
				using CommandStep = typename Storage<personality>::CommandStep;

				if(
					Storage<personality>::next_command_step_ == CommandStep::None ||
					access_column < Storage<personality>::minimum_command_column_
				) {
					return;
				}

				auto &context = Storage<personality>::command_context_;
				const uint8_t *const source = (context.arguments & 0x10) ? Storage<personality>::expansion_ram_.data() : ram_.data();
				const AddressT source_mask = (context.arguments & 0x10) ? 0xfff : 0x1ffff;
				uint8_t *const destination = (context.arguments & 0x20) ? Storage<personality>::expansion_ram_.data() : ram_.data();
				const AddressT destination_mask = (context.arguments & 0x20) ? 0xfff : 0x1ffff;
				switch(Storage<personality>::next_command_step_) {
					// Duplicative, but keeps the compiler happy.
					case CommandStep::None:
					break;

					case CommandStep::CopySourcePixelToStatus:
						Storage<personality>::colour_status_ =
							extract_colour(
								source[command_address(context.source, context.arguments & 0x10) & source_mask],
								context.source
							);

						Storage<personality>::command_->advance();
						Storage<personality>::update_command_step(access_column);
					break;

					case CommandStep::ReadSourcePixel:
						context.latched_colour.set(
							extract_colour(
								source[command_address(context.source, context.arguments & 0x10)] & source_mask,
								context.source)
							);

						Storage<personality>::minimum_command_column_ = access_column + 32;
						Storage<personality>::next_command_step_ = CommandStep::ReadDestinationPixel;
					break;

					case CommandStep::ReadDestinationPixel:
						Storage<personality>::command_latch_ =
							source[command_address(context.destination, context.arguments & 0x20) & source_mask];

						Storage<personality>::minimum_command_column_ = access_column + 24;
						Storage<personality>::next_command_step_ = CommandStep::WritePixel;
					break;

					case CommandStep::WritePixel: {
						const auto [mask, unmasked_colour] = command_colour_mask(context.destination);
						const auto address = command_address(context.destination, context.arguments & 0x20) & destination_mask;
						const uint8_t colour = unmasked_colour & mask;
						context.latched_colour.reset();

						using LogicalOperation = CommandContext::LogicalOperation;
						if(!context.test_source || colour) {
							switch(context.pixel_operation) {
								default:
								case LogicalOperation::Copy:
									Storage<personality>::command_latch_ &= ~mask;
									Storage<personality>::command_latch_ |= colour;
								break;
								case LogicalOperation::And:
									Storage<personality>::command_latch_ &= ~mask | colour;
								break;
								case LogicalOperation::Or:
									Storage<personality>::command_latch_ |= colour;
								break;
								case LogicalOperation::Xor:
									Storage<personality>::command_latch_ ^= colour;
								break;
								case LogicalOperation::Not:
									Storage<personality>::command_latch_ &= ~mask;
									Storage<personality>::command_latch_ |= colour ^ mask;
								break;
							}
						}

						destination[address] = Storage<personality>::command_latch_;

						Storage<personality>::command_->advance();
						Storage<personality>::update_command_step(access_column);
					} break;

					case CommandStep::ReadSourceByte: {
						Vector source_vector = context.source;
						if(Storage<personality>::command_->y_only) {
							source_vector.v[0] = context.destination.v[0];
						}
						context.latched_colour.set(source[command_address(source_vector, context.arguments & 0x10) & source_mask]);

						Storage<personality>::minimum_command_column_ = access_column + 24;
						Storage<personality>::next_command_step_ = CommandStep::WriteByte;
					} break;

					case CommandStep::WriteByte:
						destination[command_address(context.destination, context.arguments & 0x20) & destination_mask]
							= context.latched_colour.has_value() ? context.latched_colour.colour : context.colour.colour;
						context.latched_colour.reset();

						Storage<personality>::command_->advance();
						Storage<personality>::update_command_step(access_column);
					break;
				}
			}

			return;
		}

		// Copy and mutate the RAM pointer.
		AddressT address = ram_pointer_;
		++ram_pointer_;

		// Determine the relevant RAM and its mask.
		uint8_t *ram = ram_.data();
		AddressT mask = memory_mask(personality);

		if constexpr (is_yamaha_vdp(personality)) {
			// The Yamaha increments only 14 bits of the address in TMS-compatible modes.
			if(this->underlying_mode_ < ScreenMode::YamahaText80) {
				ram_pointer_ = (ram_pointer_ & 0x3fff) | (address & AddressT(~0x3fff));
			}

			if(this->underlying_mode_ == ScreenMode::YamahaGraphics6 || this->underlying_mode_ == ScreenMode::YamahaGraphics7) {
				// Rotate address one to the right as the hardware accesses
				// the underlying banks of memory alternately but presents
				// them as if linear.
				address = rotate(address);
			}

			// Also check whether expansion RAM is the true target here.
			if(Storage<personality>::command_context_.arguments & 0x40) {
				ram = Storage<personality>::expansion_ram_.data();
				mask = AddressT(Storage<personality>::expansion_ram_.size() - 1);
			}
		}

		switch(queued_access_) {
			default: break;

			case MemoryAccess::Write:
				if constexpr (is_sega_vdp(personality)) {
					if(Storage<personality>::cram_is_selected_) {
						// Adjust the palette. In a Master System blue has a slightly different
						// scale; cf. https://www.retrorgb.com/sega-master-system-non-linear-blue-channel-findings.html
						constexpr uint8_t rg_scale[] = {0, 85, 170, 255};
						constexpr uint8_t b_scale[] = {0, 104, 170, 255};
						Storage<personality>::colour_ram_[address & 0x1f] = palette_pack(
							rg_scale[(read_ahead_buffer_ >> 0) & 3],
							rg_scale[(read_ahead_buffer_ >> 2) & 3],
							b_scale[(read_ahead_buffer_ >> 4) & 3]
						);

						// Schedule a CRAM dot; this is scheduled for wherever it should appear
						// on screen. So it's wherever the output stream would be now. Which
						// is output_lag cycles ago from the point of view of the input stream.
						auto &dot = Storage<personality>::upcoming_cram_dots_.emplace_back();
						dot.location.column = fetch_pointer_.column - output_lag;
						dot.location.row = fetch_pointer_.row;

						// Handle before this row conditionally; then handle after (or, more realistically,
						// exactly at the end of) naturally.
						if(dot.location.column < 0) {
							--dot.location.row;
							dot.location.column += 342;
						}
						dot.location.row += dot.location.column / 342;
						dot.location.column %= 342;

						dot.value = Storage<personality>::colour_ram_[address & 0x1f];
						break;
					}
				}
				ram[address & mask] = read_ahead_buffer_;
			break;
			case MemoryAccess::Read:
				read_ahead_buffer_ = ram[address & mask];
			break;
		}
		queued_access_ = MemoryAccess::None;
	}

	/// Helper for TMS dispatches; contains a switch statement with cases 0 to 170, each of the form:
	///
	///		if constexpr (use_end && end == n) return; [[fallthrough]]; case n: fetcher.fetch<n>();
	///
	/// i.e. it provides standard glue to enter a fetch sequence at any point, while the fetches themselves are templated on the cycle
	/// at which they appear for neater expression.
	template<bool use_end, typename Fetcher> void dispatch(Fetcher &fetcher, int start, int end);

	// Various fetchers.
	template<bool use_end> void fetch_tms_refresh(uint8_t y, int start, int end);
	template<bool use_end> void fetch_tms_text(uint8_t y, int start, int end);
	template<bool use_end> void fetch_tms_character(uint8_t y, int start, int end);

	template<bool use_end> void fetch_yamaha(uint8_t y, int start, int end);
	template<ScreenMode> void fetch_yamaha(uint8_t y, int end);

	template<bool use_end> void fetch_sms(uint8_t y, int start, int end);

	// A helper function to output the current border colour for
	// the number of cycles supplied.
	void output_border(int cycles, uint32_t cram_dot);

	// Output serialisation state.
	uint32_t *pixel_target_ = nullptr, *pixel_origin_ = nullptr;
	bool asked_for_write_area_ = false;

	// Output serialisers.
	template <SpriteMode mode = SpriteMode::Mode1> void draw_tms_character(int start, int end);
	template <bool apply_blink> void draw_tms_text(int start, int end);
	void draw_sms(int start, int end, uint32_t cram_dot);

	template<ScreenMode mode> void draw_yamaha(uint8_t y, int start, int end);
	void draw_yamaha(uint8_t y, int start, int end);

	template <SpriteMode mode, bool double_width> void draw_sprites(uint8_t y, int start, int end, const std::array<uint32_t, 16> &palette, int *colour_buffer = nullptr);
};

}

#include "Fetch.hpp"
#include "Draw.hpp"

#endif /* TMS9918Base_hpp */
