//
//  Blitter.cpp
//  Clock Signal
//
//  Created by Thomas Harte on 22/07/2021.
//  Copyright © 2021 Thomas Harte. All rights reserved.
//

#include "Blitter.hpp"

#include "Minterms.hpp"

#include <cassert>

#ifndef NDEBUG
#define NDEBUG
#endif

#define LOG_PREFIX "[Blitter] "
#include "../../Outputs/Log.hpp"

using namespace Amiga;

void Blitter::set_control(int index, uint16_t value) {
	if(index) {
		line_mode_ = (value & 0x0001);
		one_dot_ = value & 0x0002;
		line_direction_ = (value >> 2) & 7;
		line_sign_ = (value & 0x0040) ? -1 : 1;

		direction_ = one_dot_ ? uint32_t(-1) : uint32_t(1);
		exclusive_fill_ = (value & 0x0010);
		inclusive_fill_ = (value & 0x0008);
		fill_carry_ = (value & 0x0004);
	} else {
		minterms_ = value & 0xff;
		channel_enables_[3] = value & 0x100;
		channel_enables_[2] = value & 0x200;
		channel_enables_[1] = value & 0x400;
		channel_enables_[0] = value & 0x800;
	}
	shifts_[index] = value >> 12;
	LOG("Set control " << index << " to " << PADHEX(4) << value);
}

void Blitter::set_first_word_mask(uint16_t value) {
	LOG("Set first word mask: " << PADHEX(4) << value);
	a_mask_[0] = value;
}

void Blitter::set_last_word_mask(uint16_t value) {
	LOG("Set last word mask: " << PADHEX(4) << value);
	a_mask_[1] = value;
}

void Blitter::set_size(uint16_t value) {
	width_ = (width_ & ~0x3f) | (value & 0x3f);
	height_ = (height_ & ~0x3ff) | (value >> 6);
	LOG("Set size to " << std::dec << width_ << ", " << height_);

	// Current assumption: writing this register informs the
	// blitter that it should treat itself as about to start a new line.
}

void Blitter::set_minterms(uint16_t value) {
	LOG("Set minterms " << PADHEX(4) << value);
	minterms_ = value & 0xff;
}

void Blitter::set_vertical_size([[maybe_unused]] uint16_t value) {
	LOG("Set vertical size " << PADHEX(4) << value);
	// TODO. This is ECS only, I think. Ditto set_horizontal_size.
}

void Blitter::set_horizontal_size([[maybe_unused]] uint16_t value) {
	LOG("Set horizontal size " << PADHEX(4) << value);
}

void Blitter::set_data(int channel, uint16_t value) {
	LOG("Set data " << channel << " to " << PADHEX(4) << value);

	// Ugh, backed myself into a corner. TODO: clean.
	switch(channel) {
		case 0: a_data_ = value; break;
		case 1: b_data_ = value; break;
		case 2: c_data_ = value; break;
		default: break;
	}
}

uint16_t Blitter::get_status() {
	const uint16_t result =
		(not_zero_flag_ ? 0x0000 : 0x2000) | (height_ ? 0x4000 : 0x0000);
	LOG("Returned status of " << result);
	return result;
}

bool Blitter::advance() {
	if(!height_) return false;

	not_zero_flag_ = false;
	if(line_mode_) {
		// As-yet unimplemented:
		assert(b_data_ == 0xffff);

		//
		// Line mode.
		//

		// Bluffer's guide to line mode:
		//
		// In Bresenham terms, the following registers have been set up:
		//
		//	[A modulo] = 4 * (dy - dx)
		//	[B modulo] = 4 * dy
		//	[A pointer] = 4 * dy - 2 * dx, with the sign flag in BLTCON1 indicating sign.
		//
		//	[A data] = 0x8000
		//	[Both masks] = 0xffff
		//	[A shift] = x1 & 15
		//
		//	[B data] = texture
		//	[B shift] = bit at which to start the line texture (0 = LSB)
		//
		//	[C and D pointers] = word containing the first pixel of the line
		//	[C and D modulo] = width of the bitplane in bytes
		//
		//	height = number of pixels
		//
		//	If ONEDOT of BLTCON1 is set, plot only a single bit per horizontal row.
		//
		//	BLTCON1 quadrants are (bits 2–4):
		//
		//		110 -> step in x, x positive, y negative
		//		111 -> step in x, x negative, y negative
		//		101 -> step in x, x negative, y positive
		//		100 -> step in x, x positive, y positive
		//
		//		001 -> step in y, x positive, y negative
		//		011 -> step in y, x negative, y negative
		//		010 -> step in y, x negative, y positive
		//		000 -> step in y, x positive, y positive
		//
		//	So that's:
		//
		//		* bit 4 = x [=1] or y [=0] major;
		//		* bit 3 = 1 => major variable negative; otherwise positive;
		//		* bit 2 = 1 => minor variable negative; otherwise positive.

		//
		// Implementation below is heavily based on the documentation found
		// at https://github.com/niklasekstrom/blitter-subpixel-line/blob/master/Drawing%20lines%20using%20the%20Amiga%20blitter.pdf
		//

		int error = int16_t(pointer_[0] << 1) >> 1;	// TODO: what happens if line_sign_ doesn't agree with this?
		bool draw_ = true;
		while(height_--) {

			if(draw_) {
				// TODO: patterned lines. Unclear what to do with the bit that comes out of b.
				// Probably extend it to a full word?
				c_data_ = ram_[pointer_[3] & ram_mask_];
				const uint16_t output =
					apply_minterm<uint16_t>(a_data_ >> shifts_[0], b_data_, c_data_, minterms_);
				ram_[pointer_[3] & ram_mask_] = output;
				not_zero_flag_ |= output;
				draw_ &= !one_dot_;
			}

			constexpr int LEFT	= 1 << 0;
			constexpr int RIGHT	= 1 << 1;
			constexpr int UP	= 1 << 2;
			constexpr int DOWN	= 1 << 3;
			int step = (line_direction_ & 4) ?
				((line_direction_ & 1) ? LEFT : RIGHT) :
				((line_direction_ & 1) ? UP : DOWN);

			if(error < 0) {
				error += modulos_[1];
			} else {
				step |=
					(line_direction_ & 4) ?
						((line_direction_ & 2) ? UP : DOWN) :
						((line_direction_ & 2) ? LEFT : RIGHT);

				error += modulos_[0];
			}

			if(step & LEFT) {
				--shifts_[0];
				if(shifts_[0] == -1) {
					--pointer_[3];
				}
			} else if(step & RIGHT) {
				++shifts_[0];
				if(shifts_[0] == 16) {
					++pointer_[3];
				}
			}
			shifts_[0] &= 15;

			if(step & UP) {
				pointer_[3] -= modulos_[2];
				draw_ = true;
			} else if(step & DOWN) {
				pointer_[3] += modulos_[2];
				draw_ = true;
			}
		}
	} else {
		// Copy mode.

		// Quick hack: do the entire action atomically.
		a32_ = 0;
		b32_ = 0;

		for(int y = 0; y < height_; y++) {
			bool fill_carry = fill_carry_;

			for(int x = 0; x < width_; x++) {
				uint16_t a_mask = 0xffff;
				if(x == 0) a_mask &= a_mask_[0];
				if(x == width_ - 1) a_mask &= a_mask_[1];

				if(channel_enables_[0]) {
					a_data_ = ram_[pointer_[0] & ram_mask_];
					pointer_[0] += direction_;
				}
				a32_ = (a32_ << 16) | (a_data_ & a_mask);

				if(channel_enables_[1]) {
					b_data_ = ram_[pointer_[1] & ram_mask_];
					pointer_[1] += direction_;
				}
				b32_ = (b32_ << 16) | b_data_;

				if(channel_enables_[2]) {
					c_data_ = ram_[pointer_[2] & ram_mask_];
					pointer_[2] += direction_;
				}

				uint16_t a, b;

				// The barrel shifter shifts to the right in ascending address mode,
				// but to the left othrwise
				if(!one_dot_) {
					a = uint16_t(a32_ >> shifts_[0]);
					b = uint16_t(b32_ >> shifts_[1]);
				} else {
					// TODO: there must be a neater solution than this.
					a = uint16_t(
						(a32_ << shifts_[0]) |
						(a32_ >> (32 - shifts_[0]))
					);

					b = uint16_t(
						(b32_ << shifts_[1]) |
						(b32_ >> (32 - shifts_[1]))
					);
				}

				uint16_t output =
					apply_minterm<uint16_t>(
						a,
						b,
						c_data_,
						minterms_);

				// TODO: don't be so dense as below. This is the initial
				// does-it-pass-the-tests? version.
				if(exclusive_fill_ || inclusive_fill_) {
					uint16_t fill_output = 0;
					uint16_t bit = one_dot_ ? 0x0001 : 0x8000;
					uint16_t flag = fill_carry ? bit : 0x0000;
					while(bit) {
						if(exclusive_fill_) flag ^= (output & bit);
						if(inclusive_fill_) flag ^= (output & bit & ~flag);	// Accept bits that would transition to set immediately.
						fill_output |= flag;
						if(inclusive_fill_) flag ^= (output & bit & flag);	// Accept bits that would transition to clear after the fact.

						fill_carry = flag;
						if(one_dot_) {
							bit <<= 1;
							flag <<= 1;
						} else {
							bit >>= 1;
							flag >>= 1;
						}
					}

					output = fill_output;
				}

				not_zero_flag_ |= output;

				if(channel_enables_[3]) {
					ram_[pointer_[3] & ram_mask_] = output;
					pointer_[3] += direction_;
				}
			}

			pointer_[0] += modulos_[0] * channel_enables_[0] * direction_;
			pointer_[1] += modulos_[1] * channel_enables_[1] * direction_;
			pointer_[2] += modulos_[2] * channel_enables_[2] * direction_;
			pointer_[3] += modulos_[3] * channel_enables_[3] * direction_;
		}
	}

	posit_interrupt(InterruptFlag::Blitter);
	height_ = 0;

	return true;
}
