//
//  Disk.cpp
//  Clock Signal
//
//  Created by Thomas Harte on 02/11/2021.
//  Copyright © 2021 Thomas Harte. All rights reserved.
//

#include "Chipset.hpp"

#ifndef NDEBUG
#define NDEBUG
#endif

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

using namespace Amiga;

// MARK: - Disk DMA.

void Chipset::DiskDMA::enqueue(uint16_t value, bool matches_sync) {
	if(matches_sync && state_ == State::WaitingForSync) {
		state_ = State::Reading;
		return;
	}

	if(state_ == State::Reading) {
		buffer_[buffer_write_ & 3] = value;
		if(buffer_write_ == buffer_read_ + 4) {
			++buffer_read_;
		}
		++buffer_write_;
	}
}

void Chipset::DiskDMA::set_control(uint16_t control) {
	sync_with_word_ = control & 0x400;
}

void Chipset::DiskDMA::set_length(uint16_t value) {
	if(value == last_set_length_) {
		dma_enable_ = value & 0x8000;
		write_ = value & 0x4000;
		length_ = value & 0x3fff;
		buffer_read_ = buffer_write_ = 0;

		if(dma_enable_) {
			LOG("Disk DMA " << (write_ ? "write" : "read") << " of " << length_ << " to " << PADHEX(8) << pointer_[0]);
		}

		state_ = sync_with_word_ ? State::WaitingForSync : State::Reading;
	}

	last_set_length_ = value;
}

bool Chipset::DiskDMA::advance_dma() {
	if(!dma_enable_) return false;

	if(!write_) {
		if(length_ && buffer_read_ != buffer_write_) {
			ram_[pointer_[0] & ram_mask_] = buffer_[buffer_read_ & 3];
			++pointer_[0];
			++buffer_read_;
			--length_;

			if(!length_) {
				chipset_.posit_interrupt(InterruptFlag::DiskBlock);
				state_ = State::Inactive;
			}

			return true;
		}
	}

	return false;
}

// MARK: - Disk Controller.

Chipset::DiskController::DiskController(Cycles clock_rate, Chipset &chipset, DiskDMA &disk_dma, CIAB &cia) :
	Storage::Disk::Controller(clock_rate),
	chipset_(chipset),
	disk_dma_(disk_dma),
	cia_(cia) {

	// Add four drives.
	for(int c = 0; c < 4; c++) {
		emplace_drive(clock_rate.as<int>(), 300, 2, Storage::Disk::Drive::ReadyType::IBMRDY);
	}
}

void Chipset::DiskController::process_input_bit(int value) {
	data_ = uint16_t((data_ << 1) | value);
	++bit_count_;

	const bool sync_matches = data_ == sync_word_;
	if(sync_matches) {
		chipset_.posit_interrupt(InterruptFlag::DiskSyncMatch);

		if(sync_with_word_) {
			bit_count_ = 0;
		}
	}

	if(!(bit_count_ & 15)) {
		disk_dma_.enqueue(data_, sync_matches);
	}
}

void Chipset::DiskController::set_sync_word(uint16_t value) {
	LOG("Set disk sync word to " << PADHEX(4) << value);
	sync_word_ = value;
}

void Chipset::DiskController::set_control(uint16_t control) {
	// b13 and b14: precompensation length specifier
	// b12: 0 => GCR precompensation; 1 => MFM.
	// b10: 1 => enable use of word sync; 0 => disable.
	// b9: 1 => sync on MSB (Disk II style, presumably?); 0 => don't.
	// b8: 1 => 2µs per bit; 0 => 4µs.

	sync_with_word_ = control & 0x400;

	Storage::Time bit_length;
	bit_length.length = 1;
	bit_length.clock_rate = (control & 0x100) ? 500000 : 250000;
	set_expected_bit_length(bit_length);

	LOG((sync_with_word_ ? "Will" : "Won't") << " sync with word; bit length is " << ((control & 0x100) ? "short" : "long"));
}

void Chipset::DiskController::process_index_hole() {
	// Pulse the CIA flag input.
	//
	// TODO: rectify once drives do an actual index pulse, with length.
	cia_.set_flag_input(true);
	cia_.set_flag_input(false);

	// Resync word output. Experimental!!
	bit_count_ = 0;
}

void Chipset::DiskController::set_mtr_sel_side_dir_step(uint8_t value) {
	// b7: /MTR
	// b6: /SEL3
	// b5: /SEL2
	// b4: /SEL1
	// b3: /SEL0
	// b2: /SIDE
	// b1: DIR
	// b0: /STEP

	// Select active drive.
	set_drive(((value >> 3) & 0x0f) ^ 0x0f);

	// "[The MTR] signal is nonstandard on the Amiga system.
	// Each drive will latch the motor signal at the time its
	// select signal turns on." — The Hardware Reference Manual.
	const auto difference = int(previous_select_ ^ value);
	previous_select_ = value;

	// Check for changes in the SEL line per drive.
	const bool motor_on = !(value & 0x80);
	const int side = (value & 0x04) ? 0 : 1;
	const bool did_step = difference & value & 0x01;
	const auto direction = Storage::Disk::HeadPosition(
		(value & 0x02) ? -1 : 1
	);

	for(int c = 0; c < 4; c++) {
		auto &drive = get_drive(size_t(c));
		const int select_mask = 0x08 << c;
		const bool is_selected = !(value & select_mask);

		// Both the motor state and the ID shifter are affected upon
		// changes in drive selection only.
		if(difference & select_mask) {
			// If transitioning to inactive, shift the drive ID value;
			// if transitioning to active, possibly reset the drive
			// ID and definitely latch the new motor state.
			if(!is_selected) {
				drive_ids_[c] <<= 1;
				LOG("Shifted drive ID shift register for drive " << +c << " to " << PADHEX(4) << std::bitset<16>{drive_ids_[c]});
			} else {
				// Motor transition on -> off => reload register.
				if(!motor_on && drive.get_motor_on()) {
					// NB:
					//	0xffff'ffff	= 3.5" drive;
					//	0x5555'5555 = 5.25" drive;
					//	0x0000'0000 = no drive.
					drive_ids_[c] = 0xffff'ffff;
					LOG("Reloaded drive ID shift register for drive " << +c);
				}

				// Also latch the new motor state.
				drive.set_motor_on(motor_on);
			}
		}

		// Set the new side.
		drive.set_head(side);

		// Possibly step.
		if(did_step && is_selected) {
			LOG("Stepped drive " << +c << " by " << std::dec << +direction.as_int());
			drive.step(direction);
		}
	}
}

uint8_t Chipset::DiskController::get_rdy_trk0_wpro_chng() {
	//	b5:	/RDY
	//	b4:	/TRK0
	//	b3:	/WPRO
	//	b2:	/CHNG

	// My interpretation:
	//
	//	RDY isn't RDY, it's a shift value as described above, combined with the motor state.
	//	CHNG is what is normally RDY.

	const uint32_t combined_id =
		((previous_select_ & 0x40) ? 0 : drive_ids_[3]) |
		((previous_select_ & 0x20) ? 0 : drive_ids_[2]) |
		((previous_select_ & 0x10) ? 0 : drive_ids_[1]) |
		((previous_select_ & 0x08) ? 0 : drive_ids_[0]);

	auto &drive = get_drive();
	const uint8_t active_high =
		((combined_id & 0x8000) >> 10) |
		(drive.get_motor_on() ? 0x20 : 0x00) |
		(drive.get_is_ready() ? 0x00 : 0x04) |
		(drive.get_is_track_zero() ? 0x10 : 0x00) |
		(drive.get_is_read_only() ? 0x08 : 0x00);

	return ~active_high;
}

void Chipset::DiskController::set_activity_observer(Activity::Observer *observer) {
	for_all_drives([observer] (Storage::Disk::Drive &drive, size_t index) {
		drive.set_activity_observer(observer, "Drive " + std::to_string(index+1), true);
	});
}

bool Chipset::DiskController::insert(const std::shared_ptr<Storage::Disk::Disk> &disk, size_t drive) {
	if(drive >= 4) return false;
	get_drive(drive).set_disk(disk);
	return true;
}

bool Chipset::insert(const std::vector<std::shared_ptr<Storage::Disk::Disk>> &disks) {
	bool inserted = false;

	size_t target = 0;
	for(const auto &disk: disks) {
		inserted |= disk_controller_.insert(disk, target);
		++target;
	}

	return inserted;
}
