//
//  CRTC6845.hpp
//  Clock Signal
//
//  Created by Thomas Harte on 31/07/2017.
//  Copyright 2017 Thomas Harte. All rights reserved.
//

#pragma once

#include "../../ClockReceiver/ClockReceiver.hpp"

#include <cstdint>
#include <cstdio>

namespace Motorola::CRTC {

struct BusState {
	bool display_enable = false;
	bool hsync = false;
	bool vsync = false;
	bool cursor = false;
	uint16_t refresh_address = 0;
	uint16_t row_address = 0;

	// Not strictly part of the bus state; provided because the partition between 6845 and bus handler
	// doesn't quite hold up in some emulated systems where the two are integrated and share more state.
	int field_count = 0;
};

class BusHandler {
	public:
		/*!
			Performs the first phase of a 6845 bus cycle; this is the phase in which it is intended that
			systems using the 6845 respect the bus state and produce pixels, sync or whatever they require.
		*/
		void perform_bus_cycle_phase1(const BusState &) {}

		/*!
			Performs the second phase of a 6845 bus cycle. Some bus state, including sync, is updated
			directly after phase 1 and hence is visible to an observer during phase 2. Handlers may therefore
			implement @c perform_bus_cycle_phase2 to be notified of the availability of that state without
			having to wait until the next cycle has begun.
		*/
		void perform_bus_cycle_phase2(const BusState &) {}
};

enum class Personality {
	HD6845S,	// Type 0 in CPC parlance. Zero-width HSYNC available, no status, programmable VSYNC length.
				// Considered exactly identical to the UM6845, so this enum covers both.
	UM6845R,	// Type 1 in CPC parlance. Status register, fixed-length VSYNC.
	MC6845,		// Type 2. No status register, fixed-length VSYNC, no zero-length HSYNC.
	AMS40226,	// Type 3. Status is get register, fixed-length VSYNC, no zero-length HSYNC.

	EGA,		// Extended EGA-style CRTC; uses 16-bit addressing throughout.
};

constexpr bool is_egavga(Personality p) {
	return p >= Personality::EGA;
}

// https://www.pcjs.org/blog/2018/03/20/ advises that "the behavior of bits 5 and 6 [of register 10, the cursor start
// register is really card specific".
//
// This enum captures those specifics.
enum class CursorType {
	/// No cursor signal is generated.
	None,
	/// MDA style: 00 => symmetric blinking; 01 or 10 => no blinking; 11 => short on, long off.
	MDA,
	/// EGA style: ignore the bits completely.
	EGA,
};

// TODO UM6845R and R12/R13; see http://www.cpcwiki.eu/index.php/CRTC#CRTC_Differences

template <class BusHandlerT, Personality personality, CursorType cursor_type> class CRTC6845 {
	public:
		CRTC6845(BusHandlerT &bus_handler) noexcept :
			bus_handler_(bus_handler), status_(0) {}

		void select_register(uint8_t r) {
			selected_register_ = r;
		}

		uint8_t get_status() {
			switch(personality) {
				case Personality::UM6845R:	return status_ | (bus_state_.vsync ? 0x20 : 0x00);
				case Personality::AMS40226:	return get_register();
				default:					return 0xff;
			}
			return 0xff;
		}

		uint8_t get_register() {
			if(selected_register_ == 31) status_ &= ~0x80;
			if(selected_register_ == 16 || selected_register_ == 17) status_ &= ~0x40;

			if(personality == Personality::UM6845R && selected_register_ == 31) return dummy_register_;
			if(selected_register_ < 12 || selected_register_ > 17) return 0xff;
			return registers_[selected_register_];
		}

		void set_register(uint8_t value) {
			static constexpr bool is_ega = is_egavga(personality);

			auto load_low = [value](uint16_t &target) {
				target = (target & 0xff00) | value;
			};
			auto load_high = [value](uint16_t &target) {
				constexpr uint8_t mask = RefreshMask >> 8;
				target = uint16_t((target & 0x00ff) | ((value & mask) << 8));
			};

			switch(selected_register_) {
				case 0:	layout_.horizontal.total = value;		break;
				case 1: layout_.horizontal.displayed = value;	break;
				case 2:	layout_.horizontal.start_sync = value;	break;
				case 3:
					layout_.horizontal.sync_width = value & 0xf;
					layout_.vertical.sync_lines = value >> 4;
					// TODO: vertical sync lines:
					// "(0 means 16 on some CRTC. Not present on all CRTCs, fixed to 16 lines on these)"
				break;
				case 4:	layout_.vertical.total = value & 0x7f;		break;
				case 5:	layout_.vertical.adjust = value & 0x1f;		break;
				case 6:	layout_.vertical.displayed = value & 0x7f;	break;
				case 7:	layout_.vertical.start_sync = value & 0x7f;	break;
				case 8:
					switch(value & 3) {
						default:	layout_.interlace_mode_ = InterlaceMode::Off;					break;
						case 0b01:	layout_.interlace_mode_ = InterlaceMode::InterlaceSync;			break;
						case 0b11:	layout_.interlace_mode_ = InterlaceMode::InterlaceSyncAndVideo;	break;
					}

					// Per CPC documentation, skew doesn't work on a "type 1 or 2", i.e. an MC6845 or a UM6845R.
					if(personality != Personality::UM6845R && personality != Personality::MC6845) {
						switch((value >> 4)&3) {
							default:	display_skew_mask_ = 1;		break;
							case 1:		display_skew_mask_ = 2;		break;
							case 2:		display_skew_mask_ = 4;		break;
						}
					}
				break;
				case 9:	layout_.vertical.end_row = value & 0x1f;	break;
				case 10:
					layout_.vertical.start_cursor = value & 0x1f;
					layout_.cursor_flags = (value >> 5) & 3;
				break;
				case 11:
					layout_.vertical.end_cursor = value & 0x1f;
				break;
				case 12:	load_high(layout_.start_address);	break;
				case 13:	load_low(layout_.start_address);	break;
				case 14:	load_high(layout_.cursor_address);	break;
				case 15:	load_low(layout_.cursor_address);	break;
			}

			static constexpr uint8_t masks[] = {
				0xff,	// Horizontal total.
				0xff,	// Horizontal display end.
				0xff,	// Start horizontal blank.
				0xff,	//
						// EGA: b0–b4: end of horizontal blank;
						// b5–b6: "Number of character clocks to delay start of display after Horizontal Total has been reached."

				is_ega ? 0xff : 0x7f,	// Start horizontal retrace.
				0x1f, 0x7f, 0x7f,
				0xff, 0x1f, 0x7f, 0x1f,
				uint8_t(RefreshMask >> 8), uint8_t(RefreshMask),
				uint8_t(RefreshMask >> 8), uint8_t(RefreshMask),
			};

			if(selected_register_ < 16) {
				registers_[selected_register_] = value & masks[selected_register_];
			}
			if(selected_register_ == 31 && personality == Personality::UM6845R) {
				dummy_register_ = value;
			}
		}

		void trigger_light_pen() {
			registers_[17] = bus_state_.refresh_address & 0xff;
			registers_[16] = bus_state_.refresh_address >> 8;
			status_ |= 0x40;
		}

		void run_for(Cycles cycles) {
			auto cyles_remaining = cycles.as_integral();
			while(cyles_remaining--) {
				// Check for end of visible characters.
				if(character_counter_ == layout_.horizontal.displayed) {
					// TODO: consider skew in character_is_visible_. Or maybe defer until perform_bus_cycle?
					character_is_visible_ = false;
					end_of_line_address_ = bus_state_.refresh_address;
				}

				perform_bus_cycle_phase1();
				bus_state_.refresh_address = (bus_state_.refresh_address + 1) & RefreshMask;

				bus_state_.cursor = is_cursor_line_ &&
					bus_state_.refresh_address == layout_.cursor_address;

				// Check for end-of-line.
				if(character_counter_ == layout_.horizontal.total) {
					character_counter_ = 0;
					do_end_of_line();
					character_is_visible_ = true;
				} else {
					// Increment counter.
					character_counter_++;
				}

				// Check for start of horizontal sync.
				if(character_counter_ == layout_.horizontal.start_sync) {
					hsync_counter_ = 0;
					bus_state_.hsync = true;
				}

				// Check for end of horizontal sync; note that a sync time of zero will result in an immediate
				// cancellation of the plan to perform sync if this is an HD6845S or UM6845R; otherwise zero
				// will end up counting as 16 as it won't be checked until after overflow.
				if(bus_state_.hsync) {
					switch(personality) {
						case Personality::HD6845S:
						case Personality::UM6845R:
							bus_state_.hsync = hsync_counter_ != layout_.horizontal.sync_width;
							hsync_counter_ = (hsync_counter_ + 1) & 15;
						break;
						default:
							hsync_counter_ = (hsync_counter_ + 1) & 15;
							bus_state_.hsync = hsync_counter_ != layout_.horizontal.sync_width;
						break;
					}
				}

				perform_bus_cycle_phase2();
			}
		}

		const BusState &get_bus_state() const {
			return bus_state_;
		}

	private:
		static constexpr uint16_t RefreshMask = (personality >= Personality::EGA) ? 0xffff : 0x3fff;

		inline void perform_bus_cycle_phase1() {
			// Skew theory of operation: keep a history of the last three states, and apply whichever is selected.
			character_is_visible_shifter_ = (character_is_visible_shifter_ << 1) | unsigned(character_is_visible_);
			bus_state_.display_enable = (int(character_is_visible_shifter_) & display_skew_mask_) && line_is_visible_;
			bus_handler_.perform_bus_cycle_phase1(bus_state_);
		}

		inline void perform_bus_cycle_phase2() {
			bus_handler_.perform_bus_cycle_phase2(bus_state_);
		}

		inline void do_end_of_line() {
			if constexpr (cursor_type != CursorType::None) {
				// Check for cursor disable.
				// TODO: this is handled differently on the EGA, should I ever implement that.
				is_cursor_line_ &= bus_state_.row_address != layout_.vertical.end_cursor;
			}

			// Check for end of vertical sync.
			if(bus_state_.vsync) {
				vsync_counter_ = (vsync_counter_ + 1) & 15;
				// On the UM6845R and AMS40226, honour the programmed vertical sync time; on the other CRTCs
				// always use a vertical sync count of 16.
				switch(personality) {
					case Personality::HD6845S:
					case Personality::AMS40226:
						bus_state_.vsync = vsync_counter_ != layout_.vertical.sync_lines;
					break;
					default:
						bus_state_.vsync = vsync_counter_ != 0;
					break;
				}
			}

			if(is_in_adjustment_period_) {
				line_counter_++;
				if(line_counter_ == layout_.vertical.adjust) {
					is_in_adjustment_period_ = false;
					do_end_of_frame();
				}
			} else {
				// Advance vertical counter.
				if(bus_state_.row_address == layout_.vertical.end_row) {
					bus_state_.row_address = 0;
					line_address_ = end_of_line_address_;

					// Check for entry into the overflow area.
					if(line_counter_ == layout_.vertical.total) {
						if(layout_.vertical.adjust) {
							line_counter_ = 0;
							is_in_adjustment_period_ = true;
						} else {
							do_end_of_frame();
						}
					} else {
						line_counter_ = (line_counter_ + 1) & 0x7f;
					}

					// Check for start of vertical sync.
					if(line_counter_ == layout_.vertical.start_sync) {
						bus_state_.vsync = true;
						vsync_counter_ = 0;
					}

					// Check for end of visible lines.
					if(line_counter_ == layout_.vertical.displayed) {
						line_is_visible_ = false;
					}
				} else {
					bus_state_.row_address = (bus_state_.row_address + 1) & 0x1f;
				}
			}

			bus_state_.refresh_address = line_address_;
			character_counter_ = 0;
			character_is_visible_ = (layout_.horizontal.displayed != 0);

			if constexpr (cursor_type != CursorType::None) {
				// Check for cursor enable.
				is_cursor_line_ |= bus_state_.row_address == layout_.vertical.start_cursor;

				switch(cursor_type) {
					// MDA-style blinking.
					// https://retrocomputing.stackexchange.com/questions/27803/what-are-the-blinking-rates-of-the-caret-and-of-blinking-text-on-pc-graphics-car
					// gives an 8/8 pattern for regular blinking though mode 11 is then just a guess.
					case CursorType::MDA:
						switch(layout_.cursor_flags) {
							case 0b11: is_cursor_line_ &= (bus_state_.field_count & 8) < 3;	break;
							case 0b00: is_cursor_line_ &= bool(bus_state_.field_count & 8);	break;
							case 0b01: is_cursor_line_ = false;								break;
							case 0b10: is_cursor_line_ = true;								break;
							default: break;
						}
					break;
				}
			}
		}

		inline void do_end_of_frame() {
			line_counter_ = 0;
			line_is_visible_ = true;
			line_address_ = layout_.start_address;
			bus_state_.refresh_address = line_address_;
			++bus_state_.field_count;
		}

		BusHandlerT &bus_handler_;
		BusState bus_state_;

		enum class InterlaceMode {
			Off,
			InterlaceSync,
			InterlaceSyncAndVideo,
		};
		enum class BlinkMode {
			// TODO.
		};
		struct {
			struct {
				uint8_t total;
				uint8_t displayed;
				uint8_t start_sync;
				uint8_t sync_width;
			} horizontal;

			struct {
				uint8_t total;
				uint8_t displayed;
				uint8_t start_sync;
				uint8_t sync_lines;
				uint8_t adjust;

				uint8_t end_row;
				uint8_t start_cursor;
				uint8_t end_cursor;
			} vertical;

			InterlaceMode interlace_mode_ = InterlaceMode::Off;
			uint16_t start_address;
			uint16_t cursor_address;
			uint16_t light_pen_address;
			uint8_t cursor_flags;
		} layout_;

		uint8_t registers_[18]{};
		uint8_t dummy_register_ = 0;
		int selected_register_ = 0;

		uint8_t character_counter_ = 0;
		uint8_t line_counter_ = 0;

		bool character_is_visible_ = false, line_is_visible_ = false;

		int hsync_counter_ = 0;
		int vsync_counter_ = 0;
		bool is_in_adjustment_period_ = false;

		uint16_t line_address_ = 0;
		uint16_t end_of_line_address_ = 0;
		uint8_t status_ = 0;

		int display_skew_mask_ = 1;
		unsigned int character_is_visible_shifter_ = 0;

		bool is_cursor_line_ = false;
};

}