using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using BizHawk.Emulation.Cores.Consoles.Nintendo.Gameboy;
using BizHawk.Emulation.Common;

namespace BizHawk.Emulation.Cores.Nintendo.Gameboy
{
	/// <summary>
	/// Emulate the gameboy printer in managed code
	/// </summary>
	public class GambattePrinter
	{
		// A loose c->c# port of SameBoy's printer code

		enum CommandState : byte
		{
			GB_PRINTER_COMMAND_MAGIC1,
			GB_PRINTER_COMMAND_MAGIC2,
			GB_PRINTER_COMMAND_ID,
			GB_PRINTER_COMMAND_COMPRESSION,
			GB_PRINTER_COMMAND_LENGTH_LOW,
			GB_PRINTER_COMMAND_LENGTH_HIGH,
			GB_PRINTER_COMMAND_DATA,
			GB_PRINTER_COMMAND_CHECKSUM_LOW,
			GB_PRINTER_COMMAND_CHECKSUM_HIGH,
			GB_PRINTER_COMMAND_ACTIVE,
			GB_PRINTER_COMMAND_STATUS,
		}
		enum CommandID : byte
		{
			GB_PRINTER_INIT_COMMAND = 1,
			GB_PRINTER_START_COMMAND = 2,
			GB_PRINTER_DATA_COMMAND = 4,
			GB_PRINTER_NOP_COMMAND = 0xF,
		}

		const int GB_PRINTER_MAX_COMMAND_LENGTH = 0x280;
		const int GB_PRINTER_DATA_SIZE = 0x280;

		const ushort SerialIRQAddress = 0x58;

		Gameboy gb;
		PrinterCallback callback;
		LibGambatte.LinkCallback linkCallback;

		CommandState command_state;
		CommandID command_id;

		bool compression;
		ushort length_left;
		byte[] command_data = new byte[GB_PRINTER_MAX_COMMAND_LENGTH];
		ushort command_length;
		ushort checksum;
		byte status;

		byte[] image = new byte[160 * 200];
		ushort image_offset;

		byte compression_run_lenth;
		bool compression_run_is_compressed;

		public GambattePrinter(Gameboy gb, PrinterCallback callback)
		{
			this.gb = gb;
			this.callback = callback;

			linkCallback = OnSerial;
			LibGambatte.gambatte_setlinkcallback(gb.GambatteState, linkCallback);

			// connect the cable
			LibGambatte.gambatte_linkstatus(gb.GambatteState, 259);
		}

		public void Disconnect()
		{
			if (gb.GambatteState != IntPtr.Zero)
				LibGambatte.gambatte_setlinkcallback(gb.GambatteState, null);
		}

		void OnSerial()
		{
			if (LibGambatte.gambatte_linkstatus(gb.GambatteState, 256) != 0) // ClockTrigger
			{
				LibGambatte.gambatte_linkstatus(gb.GambatteState, 257); // ack

				byte output = HandleSerial((byte)LibGambatte.gambatte_linkstatus(gb.GambatteState, 258)); // GetOut
				LibGambatte.gambatte_linkstatus(gb.GambatteState, output); // ShiftIn
			}
		}

		byte HandleSerial(byte byte_received)
		{
			byte byte_to_send = 0;

			switch (command_state)
			{
				case CommandState.GB_PRINTER_COMMAND_MAGIC1:
					if (byte_received != 0x88)
					{
						return byte_to_send;
					}
					status &= 254;
					command_length = 0;
					checksum = 0;
					break;

				case CommandState.GB_PRINTER_COMMAND_MAGIC2:
					if (byte_received != 0x33)
					{
						if (byte_received != 0x88)
						{
							command_state = CommandState.GB_PRINTER_COMMAND_MAGIC1;
						}
						return byte_to_send;
					}
					break;

				case CommandState.GB_PRINTER_COMMAND_ID:
					command_id = (CommandID)(byte_received & 0xF);
					break;

				case CommandState.GB_PRINTER_COMMAND_COMPRESSION:
					compression = (byte_received & 1) != 0;
					break;

				case CommandState.GB_PRINTER_COMMAND_LENGTH_LOW:
					length_left = byte_received;
					break;

				case CommandState.GB_PRINTER_COMMAND_LENGTH_HIGH:
					length_left |= (ushort)((byte_received & 3) << 8);
					break;

				case CommandState.GB_PRINTER_COMMAND_DATA:
					if (command_length != GB_PRINTER_MAX_COMMAND_LENGTH)
					{
						if (compression)
						{
							if (compression_run_lenth == 0)
							{
								compression_run_is_compressed = (byte_received & 0x80) != 0;
								compression_run_lenth = (byte)((byte_received & 0x7F) + 1 + (compression_run_is_compressed ? 1 : 0));
							}
							else if (compression_run_is_compressed)
							{
								while (compression_run_lenth > 0)
								{
									command_data[command_length++] = byte_received;
									compression_run_lenth--;
									if (command_length == GB_PRINTER_MAX_COMMAND_LENGTH)
									{
										compression_run_lenth = 0;
									}
								}
							}
							else
							{
								command_data[command_length++] = byte_received;
								compression_run_lenth--;
							}
						}
						else
						{
							command_data[command_length++] = byte_received;
						}
					}
					length_left--;
					break;

				case CommandState.GB_PRINTER_COMMAND_CHECKSUM_LOW:
					checksum ^= byte_received;
					break;

				case CommandState.GB_PRINTER_COMMAND_CHECKSUM_HIGH:
					checksum ^= (ushort)(byte_received << 8);
					if (checksum != 0)
					{
						status |= 1; /* Checksum error*/
						command_state = CommandState.GB_PRINTER_COMMAND_MAGIC1;
						return byte_to_send;
					}
					break;

				case CommandState.GB_PRINTER_COMMAND_ACTIVE:
					byte_to_send = 0x81;
					break;

				case CommandState.GB_PRINTER_COMMAND_STATUS:

					if (((int)command_id & 0xF) == (byte)CommandID.GB_PRINTER_INIT_COMMAND)
					{
						/* Games expect INIT commands to return 0? */
						byte_to_send = 0;
					}
					else
					{
						byte_to_send = status;
					}

					/* Printing is done instantly, but let the game recieve a 6 (Printing) status at least once, for compatibility */
					if (status == 6)
					{
						status = 4; /* Done */
					}

					command_state = CommandState.GB_PRINTER_COMMAND_MAGIC1;
					HandleCommand();
					return byte_to_send;
			}

			if (command_state >= CommandState.GB_PRINTER_COMMAND_ID && command_state < CommandState.GB_PRINTER_COMMAND_CHECKSUM_LOW)
			{
				checksum += byte_received;
			}

			if (command_state != CommandState.GB_PRINTER_COMMAND_DATA)
			{
				command_state++;
			}

			if (command_state == CommandState.GB_PRINTER_COMMAND_DATA)
			{
				if (length_left == 0)
				{
					command_state++;
				}
			}

			return byte_to_send;
		}

		void HandleCommand()
		{
			switch (command_id)
			{
				case CommandID.GB_PRINTER_INIT_COMMAND:
					status = 0;
					image_offset = 0;
					break;

				case CommandID.GB_PRINTER_START_COMMAND:
					if (command_length == 4)
					{
						status = 6; /* Printing */
						uint[] outputImage = new uint[image_offset];

						int palette = command_data[2];
						uint[] colors = new uint[] {
							0xFFFFFFFFU,
							0xFFAAAAAAU,
							0xFF555555U,
							0xFF000000U
						};
						for (int i = 0; i < image_offset; i++)
						{
							outputImage[i] = colors[(palette >> (image[i] * 2)) & 3];
						}

						if (callback != null)
						{
							// The native-friendly callback almost seems silly now :P
							unsafe
							{
								fixed (uint* imagePtr = outputImage)
								{
									callback((IntPtr)imagePtr, (byte)(image_offset / 160),
										(byte)(command_data[1] >> 4), (byte)(command_data[1] & 7),
										(byte)(command_data[3] & 0x7F));
								}
							}
						}

						image_offset = 0;
					}
					break;

				case CommandID.GB_PRINTER_DATA_COMMAND:
					if (command_length == GB_PRINTER_DATA_SIZE)
					{
						image_offset %= (ushort)image.Length;
						status = 8; /* Received 0x280 bytes */

						int data_index = 0;

						for (int row = 2; row > 0; row--)
						{
							for (int tile_x = 0; tile_x < 160 / 8; tile_x++)
							{
								for (int y = 0; y < 8; y++, data_index += 2)
								{
									for (int x_pixel = 0; x_pixel < 8; x_pixel++)
									{
										image[image_offset + tile_x * 8 + x_pixel + y * 160] =
											(byte)((command_data[data_index] >> 7) | ((command_data[data_index + 1] >> 7) << 1));
										command_data[data_index] <<= 1;
										command_data[data_index + 1] <<= 1;
									}
								}
							}

							image_offset += 8 * 160;
						}
					}
					break;

				default:
					break;
			}
		}
	}
}
