using BizHawk.Common;
using BizHawk.Common.NumberExtensions;
using BizHawk.Emulation.Common;
using System;

namespace BizHawk.Emulation.Cores.Computers.AmstradCPC
{
	/// <summary>
	/// Render pixels to the screen
	/// </summary>
	public class CRTDevice : IVideoProvider
	{
		private readonly CPCBase _machine;
		private CRCT_6845 CRCT => _machine.CRCT;
		private AmstradGateArray GateArray => _machine.GateArray;

		public CRTDevice(CPCBase machine)
		{
			_machine = machine;
			CurrentLine = new ScanLine(this);

			CRCT.AttachHSYNCCallback(OnHSYNC);
			CRCT.AttachVSYNCCallback(OnVSYNC);
		}

		/// <summary>
		/// The standard CPC Pallete (ordered by firmware #)
		/// http://www.cpcwiki.eu/index.php/CPC_Palette
		/// </summary>
		public static readonly int[] CPCFirmwarePalette =
		{
			Colors.ARGB(0x00, 0x00, 0x00), // Black
            Colors.ARGB(0x00, 0x00, 0x80), // Blue
            Colors.ARGB(0x00, 0x00, 0xFF), // Bright Blue
            Colors.ARGB(0x80, 0x00, 0x00), // Red            
            Colors.ARGB(0x80, 0x00, 0x80), // Magenta
            Colors.ARGB(0x80, 0x00, 0xFF), // Mauve
            Colors.ARGB(0xFF, 0x00, 0x00), // Bright Red
            Colors.ARGB(0xFF, 0x00, 0x80), // Purple
            Colors.ARGB(0xFF, 0x00, 0xFF), // Bright Magenta
            Colors.ARGB(0x00, 0x80, 0x00), // Green
            Colors.ARGB(0x00, 0x80, 0x80), // Cyan
            Colors.ARGB(0x00, 0x80, 0xFF), // Sky Blue
            Colors.ARGB(0x80, 0x80, 0x00), // Yellow
            Colors.ARGB(0x80, 0x80, 0x80), // White
            Colors.ARGB(0x80, 0x80, 0xFF), // Pastel Blue
            Colors.ARGB(0xFF, 0x80, 0x00), // Orange
            Colors.ARGB(0xFF, 0x80, 0x80), // Pink
            Colors.ARGB(0xFF, 0x80, 0xFF), // Pastel Magenta
            Colors.ARGB(0x00, 0xFF, 0x00), // Bright Green
            Colors.ARGB(0x00, 0xFF, 0x80), // Sea Green
            Colors.ARGB(0x00, 0xFF, 0xFF), // Bright Cyan
            Colors.ARGB(0x80, 0xFF, 0x00), // Lime
            Colors.ARGB(0x80, 0xFF, 0x80), // Pastel Green
            Colors.ARGB(0x80, 0xFF, 0xFF), // Pastel Cyan
            Colors.ARGB(0xFF, 0xFF, 0x00), // Bright Yellow
            Colors.ARGB(0xFF, 0xFF, 0x80), // Pastel Yellow
            Colors.ARGB(0xFF, 0xFF, 0xFF), // Bright White
        };

		/// <summary>
		/// The standard CPC Pallete (ordered by hardware #)
		/// http://www.cpcwiki.eu/index.php/CPC_Palette
		/// </summary>
		public static readonly int[] CPCHardwarePalette =
		{
			Colors.ARGB(0x80, 0x80, 0x80), // White
            Colors.ARGB(0x80, 0x80, 0x80), // White (duplicate)
            Colors.ARGB(0x00, 0xFF, 0x80), // Sea Green
            Colors.ARGB(0xFF, 0xFF, 0x80), // Pastel Yellow
            Colors.ARGB(0x00, 0x00, 0x80), // Blue
            Colors.ARGB(0xFF, 0x00, 0x80), // Purple
            Colors.ARGB(0x00, 0x80, 0x80), // Cyan
            Colors.ARGB(0xFF, 0x80, 0x80), // Pink
            Colors.ARGB(0xFF, 0x00, 0x80), // Purple (duplicate)
            Colors.ARGB(0xFF, 0xFF, 0x80), // Pastel Yellow (duplicate)
            Colors.ARGB(0xFF, 0xFF, 0x00), // Bright Yellow
            Colors.ARGB(0xFF, 0xFF, 0xFF), // Bright White
            Colors.ARGB(0xFF, 0x00, 0x00), // Bright Red
            Colors.ARGB(0xFF, 0x00, 0xFF), // Bright Magenta
            Colors.ARGB(0xFF, 0x80, 0x00), // Orange
            Colors.ARGB(0xFF, 0x80, 0xFF), // Pastel Magenta
            Colors.ARGB(0x00, 0x00, 0x80), // Blue (duplicate)
            Colors.ARGB(0x00, 0xFF, 0x80), // Sea Green (duplicate)
            Colors.ARGB(0x00, 0xFF, 0x00), // Bright Green
            Colors.ARGB(0x00, 0xFF, 0xFF), // Bright Cyan
            Colors.ARGB(0x00, 0x00, 0x00), // Black
            Colors.ARGB(0x00, 0x00, 0xFF), // Bright Blue
            Colors.ARGB(0x00, 0x80, 0x00), // Green
            Colors.ARGB(0x00, 0x80, 0xFF), // Sky Blue
            Colors.ARGB(0x80, 0x00, 0x80), // Magenta
            Colors.ARGB(0x80, 0xFF, 0x80), // Pastel Green
            Colors.ARGB(0x80, 0xFF, 0x00), // Lime
            Colors.ARGB(0x80, 0xFF, 0xFF), // Pastel Cyan
            Colors.ARGB(0x80, 0x00, 0x00), // Red     
            Colors.ARGB(0x80, 0x00, 0xFF), // Mauve
            Colors.ARGB(0x80, 0x80, 0x00), // Yellow            
            Colors.ARGB(0x80, 0x80, 0xFF), // Pastel Blue
        };


		/// <summary>
		/// The current scanline that is being added to
		/// (will be processed and committed to the screen buffer every HSYNC)
		/// </summary>
		public ScanLine CurrentLine;

		/// <summary>
		/// The number of top border scanlines to ommit when rendering
		/// </summary>
		public int TopLinesToTrim = 20;

		/// <summary>
		/// Count of rendered scanlines this frame
		/// </summary>
		public int ScanlineCounter = 0;

		/// <summary>
		/// Video buffer processing
		/// </summary>
		public int[] ProcessVideoBuffer()
		{
			return ScreenBuffer;
		}

		/// <summary>
		/// Sets up buffers and the like at the start of a frame
		/// </summary>
		public void SetupVideo()
		{
			if (BufferHeight == 576)
				return;

			BufferWidth = 800;
			BufferHeight = 576;

			VirtualWidth = BufferWidth / 2;
			VirtualHeight = BufferHeight / 2;

			ScreenBuffer = new int[BufferWidth * BufferHeight];
		}

		/// <summary>
		/// Fired when the CRCT flags HSYNC
		/// </summary>
		public void OnHSYNC()
		{

		}

		/// <summary>
		/// Fired when the CRCT flags VSYNC
		/// </summary>
		public void OnVSYNC()
		{

		}

		/// <summary>
		/// Video output buffer
		/// </summary>
		public int[] ScreenBuffer;

		private int _virtualWidth;
		private int _virtualHeight;
		private int _bufferWidth;
		private int _bufferHeight;

		public int BackgroundColor => CPCHardwarePalette[0];

		public int VirtualWidth
		{
			get => _virtualWidth;
			set => _virtualWidth = value;
		}

		public int VirtualHeight
		{
			get => _virtualHeight;
			set => _virtualHeight = value;
		}

		public int BufferWidth
		{
			get => _bufferWidth;
			set => _bufferWidth = value;
		}

		public int BufferHeight
		{
			get => _bufferHeight;
			set => _bufferHeight = value;
		}

		public int VsyncNumerator
		{
			get => GateArray.Z80ClockSpeed * 50;
			set { }
		}

		public int VsyncDenominator => GateArray.Z80ClockSpeed;

		public int[] GetVideoBuffer() => ProcessVideoBuffer();

		public void SetupScreenSize()
		{
			BufferWidth = 1024; // 512;
			BufferHeight = 768;
			VirtualHeight = BufferHeight;
			VirtualWidth = BufferWidth;
			ScreenBuffer = new int[BufferWidth * BufferHeight];
			croppedBuffer = ScreenBuffer;
		}

		protected int[] croppedBuffer;

		public void SyncState(Serializer ser)
		{
			ser.BeginSection("CRT");
			ser.Sync("BufferWidth", ref _bufferWidth);
			ser.Sync("BufferHeight", ref _bufferHeight);
			ser.Sync("VirtualHeight", ref _virtualHeight);
			ser.Sync("VirtualWidth", ref _virtualWidth);
			ser.Sync(nameof(ScreenBuffer), ref ScreenBuffer, false);
			ser.Sync(nameof(ScanlineCounter), ref ScanlineCounter);
			ser.EndSection();
		}
	}

	/// <summary>
	/// Represents a single scanline buffer
	/// </summary>
	public class ScanLine
	{
		/// <summary>
		/// Array of character information
		/// </summary>
		public Character[] Characters;

		/// <summary>
		/// The screenmode that was set at the start of this scanline
		/// </summary>
		public int ScreenMode = 1;

		/// <summary>
		/// The scanline number (0 based)
		/// </summary>
		public int LineIndex;

		/// <summary>
		/// The calling CRT device
		/// </summary>
		private readonly CRTDevice CRT;

		public ScanLine(CRTDevice crt)
		{
			Reset();
			CRT = crt;
		}

		// To be run after scanline has been fully processed
		public void InitScanline(int screenMode, int lineIndex)
		{
			Reset();
			ScreenMode = screenMode;
			LineIndex = lineIndex;
		}

		/// <summary>
		/// Adds a single scanline character into the matrix
		/// </summary>
		public void AddScanlineCharacter(int index, RenderPhase phase, byte vid1, byte vid2, int[] pens)
		{
			if (index >= 64)
			{
				return;
			}

			switch (phase)
			{
				case RenderPhase.BORDER:
					AddBorderValue(index, CRTDevice.CPCHardwarePalette[pens[16]]);
					break;
				case RenderPhase.DISPLAY:
					AddDisplayValue(index, vid1, vid2, pens);
					break;
				default:
					AddSyncValue(index, phase);
					break;
			}
		}

		/// <summary>
		/// Adds a HSYNC, VSYNC or HSYNC+VSYNC character into the scanline
		/// </summary>
		private void AddSyncValue(int charIndex, RenderPhase phase)
		{
			Characters[charIndex].Phase = phase;
			Characters[charIndex].Pixels = new int[0];
		}

		/// <summary>
		/// Adds a border character into the scanline
		/// </summary>
		private void AddBorderValue(int charIndex, int colourValue)
		{
			Characters[charIndex].Phase = RenderPhase.BORDER;

			switch (ScreenMode)
			{
				case 0:
					Characters[charIndex].Pixels = new int[4];
					break;
				case 1:
					Characters[charIndex].Pixels = new int[8];
					break;
				case 2:
					Characters[charIndex].Pixels = new int[16];
					break;
				case 3:
					Characters[charIndex].Pixels = new int[8];
					break;
			}



			for (int i = 0; i < Characters[charIndex].Pixels.Length; i++)
			{
				Characters[charIndex].Pixels[i] = colourValue;
			}
		}

		/// <summary>
		/// Adds a display character into the scanline
		/// Pixel matrix is calculated based on the current ScreenMode
		/// </summary>
		public void AddDisplayValue(int charIndex, byte vid1, byte vid2, int[] pens)
		{
			Characters[charIndex].Phase = RenderPhase.DISPLAY;

			// generate pixels based on screen mode
			switch (ScreenMode)
			{
				// 4 bits per pixel - 2 bytes - 4 pixels (8 CRT pixels)
				// RECT
				case 0:
					Characters[charIndex].Pixels = new int[16];

					int m0Count = 0;

					int pix = vid1 & 0xaa;
					pix = ((pix & 0x80) >> 7) | ((pix & 0x08) >> 2) | ((pix & 0x20) >> 3) | ((pix & 0x02 << 2));
					Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[pix]];
					Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[pix]];
					Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[pix]];
					Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[pix]];
					pix = vid1 & 0x55;
					pix = (((pix & 0x40) >> 6) | ((pix & 0x04) >> 1) | ((pix & 0x10) >> 2) | ((pix & 0x01 << 3)));
					Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[pix]];
					Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[pix]];
					Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[pix]];
					Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[pix]];

					pix = vid2 & 0xaa;
					pix = ((pix & 0x80) >> 7) | ((pix & 0x08) >> 2) | ((pix & 0x20) >> 3) | ((pix & 0x02 << 2));
					Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[pix]];
					Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[pix]];
					Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[pix]];
					Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[pix]];
					pix = vid2 & 0x55;
					pix = (((pix & 0x40) >> 6) | ((pix & 0x04) >> 1) | ((pix & 0x10) >> 2) | ((pix & 0x01 << 3)));
					Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[pix]];
					Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[pix]];
					Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[pix]];
					Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[pix]];
					/*
                    int m0B0P0i = vid1 & 0xaa;
                    int m0B0P0 = ((m0B0P0i & 0x80) >> 7) | ((m0B0P0i & 0x08) >> 2) | ((m0B0P0i & 0x20) >> 3) | ((m0B0P0i & 0x02 << 2));
                    int m0B0P1i = vid1 & 85;
                    int m0B0P1 = ((m0B0P1i & 0x40) >> 6) | ((m0B0P1i & 0x04) >> 1) | ((m0B0P1i & 0x10) >> 2) | ((m0B0P1i & 0x01 << 3));

                    Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[m0B0P0]];
                    Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[m0B0P0]];
                    Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[m0B0P1]];
                    Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[m0B0P1]];

                    int m0B1P0i = vid2 & 170;
                    int m0B1P0 = ((m0B1P0i & 0x80) >> 7) | ((m0B1P0i & 0x08) >> 2) | ((m0B1P0i & 0x20) >> 3) | ((m0B1P0i & 0x02 << 2));
                    int m0B1P1i = vid2 & 85;
                    int m0B1P1 = ((m0B1P1i & 0x40) >> 6) | ((m0B1P1i & 0x04) >> 1) | ((m0B1P1i & 0x10) >> 2) | ((m0B1P1i & 0x01 << 3));

                    Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[m0B1P0]];
                    Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[m0B1P0]];
                    Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[m0B1P1]];
                    Characters[charIndex].Pixels[m0Count++] = CRTDevice.CPCHardwarePalette[pens[m0B1P1]];
                    */
					break;

				// 2 bits per pixel - 2 bytes - 8 pixels (16 CRT pixels)
				// SQUARE
				case 1:
					Characters[charIndex].Pixels = new int[8];

					int m1Count = 0;

					int m1B0P0 = (((vid1 & 0x80) >> 7) | ((vid1 & 0x08) >> 2));
					int m1B0P1 = (((vid1 & 0x40) >> 6) | ((vid1 & 0x04) >> 1));
					int m1B0P2 = (((vid1 & 0x20) >> 5) | ((vid1 & 0x02)));
					int m1B0P3 = (((vid1 & 0x10) >> 4) | ((vid1 & 0x01) << 1));

					Characters[charIndex].Pixels[m1Count++] = CRTDevice.CPCHardwarePalette[pens[m1B0P0]];
					Characters[charIndex].Pixels[m1Count++] = CRTDevice.CPCHardwarePalette[pens[m1B0P1]];
					Characters[charIndex].Pixels[m1Count++] = CRTDevice.CPCHardwarePalette[pens[m1B0P2]];
					Characters[charIndex].Pixels[m1Count++] = CRTDevice.CPCHardwarePalette[pens[m1B0P3]];

					int m1B1P0 = (((vid2 & 0x80) >> 7) | ((vid2 & 0x08) >> 2));
					int m1B1P1 = (((vid2 & 0x40) >> 6) | ((vid2 & 0x04) >> 1));
					int m1B1P2 = (((vid2 & 0x20) >> 5) | ((vid2 & 0x02)));
					int m1B1P3 = (((vid2 & 0x10) >> 4) | ((vid2 & 0x01) << 1));

					Characters[charIndex].Pixels[m1Count++] = CRTDevice.CPCHardwarePalette[pens[m1B1P0]];
					Characters[charIndex].Pixels[m1Count++] = CRTDevice.CPCHardwarePalette[pens[m1B1P1]];
					Characters[charIndex].Pixels[m1Count++] = CRTDevice.CPCHardwarePalette[pens[m1B1P2]];
					Characters[charIndex].Pixels[m1Count++] = CRTDevice.CPCHardwarePalette[pens[m1B1P3]];
					break;

				// 1 bit per pixel - 2 bytes - 16 pixels (16 CRT pixels)
				// RECT
				case 2:
					Characters[charIndex].Pixels = new int[16];

					int m2Count = 0;

					int[] pixBuff = new int[16];

					for (int bit = 7; bit >= 0; bit--)
					{
						int val = vid1.Bit(bit) ? 1 : 0;
						Characters[charIndex].Pixels[m2Count++] = CRTDevice.CPCHardwarePalette[pens[val]];

					}
					for (int bit = 7; bit >= 0; bit--)
					{
						int val = vid2.Bit(bit) ? 1 : 0;
						Characters[charIndex].Pixels[m2Count++] = CRTDevice.CPCHardwarePalette[pens[val]];
					}
					break;

				// 4 bits per pixel - 2 bytes - 4 pixels (8 CRT pixels)
				// RECT
				case 3:
					Characters[charIndex].Pixels = new int[4];

					int m3Count = 0;

					int m3B0P0i = vid1 & 170;
					int m3B0P0 = ((m3B0P0i & 0x80) >> 7) | ((m3B0P0i & 0x08) >> 2) | ((m3B0P0i & 0x20) >> 3) | ((m3B0P0i & 0x02 << 2));
					int m3B0P1i = vid1 & 85;
					int m3B0P1 = ((m3B0P1i & 0x40) >> 6) | ((m3B0P1i & 0x04) >> 1) | ((m3B0P1i & 0x10) >> 2) | ((m3B0P1i & 0x01 << 3));

					Characters[charIndex].Pixels[m3Count++] = CRTDevice.CPCHardwarePalette[pens[m3B0P0]];
					Characters[charIndex].Pixels[m3Count++] = CRTDevice.CPCHardwarePalette[pens[m3B0P1]];

					int m3B1P0i = vid1 & 170;
					int m3B1P0 = ((m3B1P0i & 0x80) >> 7) | ((m3B1P0i & 0x08) >> 2) | ((m3B1P0i & 0x20) >> 3) | ((m3B1P0i & 0x02 << 2));
					int m3B1P1i = vid1 & 85;
					int m3B1P1 = ((m3B1P1i & 0x40) >> 6) | ((m3B1P1i & 0x04) >> 1) | ((m3B1P1i & 0x10) >> 2) | ((m3B1P1i & 0x01 << 3));

					Characters[charIndex].Pixels[m3Count++] = CRTDevice.CPCHardwarePalette[pens[m3B1P0]];
					Characters[charIndex].Pixels[m3Count++] = CRTDevice.CPCHardwarePalette[pens[m3B1P1]];
					break;
			}
		}

		/// <summary>
		/// Returns the number of pixels decoded in this scanline (border and display)
		/// </summary>
		private int GetPixelCount()
		{
			int cnt = 0;

			foreach (var c in Characters)
			{
				if (c.Pixels != null)
					cnt += c.Pixels.Length;
			}

			return cnt;
		}

		/// <summary>
		/// Called at the start of HSYNC
		/// Processes and adds the scanline to the Screen Buffer
		/// </summary>
		public void CommitScanline()
		{
			int hScale = 1;
			int vScale = 1;

			switch (ScreenMode)
			{
				case 0:
					hScale = 1;
					vScale = 2;
					break;
				case 1:
				case 3:
					hScale = 2;
					vScale = 2;
					break;

				case 2:
					hScale = 1;
					vScale = 2;
					break;
			}

			int hPix = GetPixelCount() * hScale;
			//int hPix = GetPixelCount() * 2;
			int leftOver = CRT.BufferWidth - hPix;
			int lPad = leftOver / 2;
			int rPad = lPad;
			int rem = leftOver % 2;
			if (rem != 0)
				rPad += rem;

			if (LineIndex < CRT.TopLinesToTrim)
			{
				return;
			}

			// render out the scanline
			int pCount = (LineIndex - CRT.TopLinesToTrim) * vScale * CRT.BufferWidth;

			// vScale
			for (int s = 0; s < vScale; s++)
			{
				// left padding
				for (int lP = 0; lP < lPad; lP++)
				{
					CRT.ScreenBuffer[pCount++] = 0;
				}

				// border and display
				foreach (var c in Characters)
				{
					if (c.Pixels == null || c.Pixels.Length == 0)
						continue;

					for (int p = 0; p < c.Pixels.Length; p++)
					{
						// hScale
						for (int h = 0; h < hScale; h++)
						{
							CRT.ScreenBuffer[pCount++] = c.Pixels[p];
						}

						//CRT.ScreenBuffer[pCount++] = c.Pixels[p];
					}
				}

				// right padding
				for (int rP = 0; rP < rPad; rP++)
				{
					CRT.ScreenBuffer[pCount++] = 0;
				}

				if (pCount != hPix)
				{

				}

				CRT.ScanlineCounter++;
			}
		}

		public void Reset()
		{
			ScreenMode = 1;
			Characters = new Character[64];

			for (int i = 0; i < Characters.Length; i++)
			{
				Characters[i] = new Character();
			}
		}
	}

	/// <summary>
	/// Contains data relating to one character written on one scanline
	/// </summary>
	public class Character
	{
		/// <summary>
		/// Array of pixels generated for this character
		/// </summary>
		public int[] Pixels;

		/// <summary>
		/// The type (NONE/BORDER/DISPLAY/HSYNC/VSYNC/HSYNC+VSYNC
		/// </summary>
		public RenderPhase Phase = RenderPhase.NONE;

		public Character()
		{
			Pixels = new int[0];
		}
	}

	[Flags]
	public enum RenderPhase : int
	{
		/// <summary>
		/// Nothing
		/// </summary>
		NONE = 0,
		/// <summary>
		/// Border is being rendered
		/// </summary>
		BORDER = 1,
		/// <summary>
		/// Display rendered from video RAM
		/// </summary>
		DISPLAY = 2,
		/// <summary>
		/// HSYNC in progress
		/// </summary>
		HSYNC = 3,
		/// <summary>
		/// VSYNC in process
		/// </summary>
		VSYNC = 4,
		/// <summary>
		/// HSYNC occurs within a VSYNC
		/// </summary>
		HSYNCandVSYNC = 5
	}
}
