using System;

namespace BizHawk.Emulation.Cores.Sega.Genesis
{
	public partial class GenVDP
	{
		// Priority buffer contents have the following values:
		// 0 = Backdrop color
		// 1 = Plane B Low Priority
		// 2 = Plane A Low Priority
		// 4 = Plane B High Priority
		// 5 = Plane A High Priority
		// 9 = Sprite has been drawn

		byte[] PriorityBuffer = new byte[320];

		static readonly byte[] PalXlatTable = { 0, 0, 36, 36, 73, 73, 109, 109, 145, 145, 182, 182, 219, 219, 255, 255 };

		public void RenderLine()
		{
			if (DisplayEnabled)
			{
				Array.Clear(PriorityBuffer, 0, 320);

				// TODO: I would like to be able to render Scroll A before Scroll B, in order to minimize overdraw.
				// But at the moment it complicates priority stuff.

				if (CellBasedVertScroll == false)
				{
					RenderScrollB();
					RenderScrollA();
				}
				else
				{
					RenderScrollBTwoCellVScroll();
					RenderScrollATwoCellVScroll();
				}

				RenderSpritesScanline();
			}
			else
			{
				// If display is disabled, fill in with background color.
				for (int i = 0; i < FrameWidth; i++)
					FrameBuffer[(ScanLine * FrameWidth) + i] = BackgroundColor;
			}

			//if (ScanLine == 223) // shrug
			//    RenderPalette();
		}

		void RenderPalette()
		{
			for (int p = 0; p < 4; p++)
				for (int i = 0; i < 16; i++)
					FrameBuffer[(p * FrameWidth) + i] = Palette[(p * 16) + i];
		}

		void RenderScrollAScanline(int xScroll, int yScroll, int nameTableBase, int startPixel, int endPixel, bool window)
		{
			const int lowPriority = 2;
			const int highPriority = 5;
			int yTile = ((ScanLine + yScroll) / 8) % NameTableHeight;
			int nameTableWidth = NameTableWidth;
			if (window)
				nameTableWidth = (DisplayWidth == 40) ? 64 : 32;

			// this is hellllla slow. but not optimizing until we implement & understand
			// all scrolling modes, shadow & hilight, etc.
			// in thinking about this, you could convince me to optimize the PCE background renderer now.
			// Its way simple in comparison. But the PCE sprite renderer is way worse than gen.
			for (int x = startPixel; x < endPixel; x++)
			{
				int xTile = Math.Abs(((x + (1024 - xScroll)) / 8) % nameTableWidth);
				int xOfs = Math.Abs((x + (1024 - xScroll)) & 7);
				int yOfs = (ScanLine + yScroll) % 8;
				int cellOfs = nameTableBase + (yTile * nameTableWidth * 2) + (xTile * 2);
				int nameTableEntry = VRAM[cellOfs] | (VRAM[cellOfs + 1] << 8);
				int patternNo = nameTableEntry & 0x7FF;
				bool hFlip = ((nameTableEntry >> 11) & 1) != 0;
				bool vFlip = ((nameTableEntry >> 12) & 1) != 0;
				bool priority = ((nameTableEntry >> 15) & 1) != 0;
				int palette = (nameTableEntry >> 13) & 3;

				if (priority && PriorityBuffer[x] >= highPriority) continue;
				if (!priority && PriorityBuffer[x] >= lowPriority) continue;

				if (vFlip) yOfs = 7 - yOfs;
				if (hFlip) xOfs = 7 - xOfs;

				int texel = PatternBuffer[(patternNo * 64) + (yOfs * 8) + (xOfs)];
				if (texel == 0) continue;
				int pixel = Palette[(palette * 16) + texel];
				FrameBuffer[(ScanLine * FrameWidth) + x] = pixel;
				PriorityBuffer[x] = (byte)(priority ? highPriority : lowPriority);
			}
		}

		void RenderScrollAScanlineTwoCellVScroll(int xScroll, int nameTableBase, int startPixel, int endPixel, bool window)
		{
			const int lowPriority = 2;
			const int highPriority = 5;

			int fineHScroll = xScroll & 15;
			int nameTableWidth = NameTableWidth;
			if (window)
				nameTableWidth = (DisplayWidth == 40) ? 64 : 32;

			for (int x = startPixel; x < endPixel; x++)
			{
				int vsramUnitOffset = ((x - fineHScroll) / 16) % 40;
				int yScroll = VSRAM[vsramUnitOffset * 2] & 0x3FF;
				int yTile = ((ScanLine + yScroll) / 8) % NameTableHeight;

				int xTile = Math.Abs(((x + (1024 - xScroll)) / 8) % nameTableWidth);
				int xOfs = Math.Abs((x + (1024 - xScroll)) & 7);
				int yOfs = (ScanLine + yScroll) % 8;
				int cellOfs = nameTableBase + (yTile * nameTableWidth * 2) + (xTile * 2);
				int nameTableEntry = VRAM[cellOfs] | (VRAM[cellOfs + 1] << 8);
				int patternNo = nameTableEntry & 0x7FF;
				bool hFlip = ((nameTableEntry >> 11) & 1) != 0;
				bool vFlip = ((nameTableEntry >> 12) & 1) != 0;
				bool priority = ((nameTableEntry >> 15) & 1) != 0;
				int palette = (nameTableEntry >> 13) & 3;

				if (priority && PriorityBuffer[x] >= highPriority) continue;
				if (!priority && PriorityBuffer[x] >= lowPriority) continue;

				if (vFlip) yOfs = 7 - yOfs;
				if (hFlip) xOfs = 7 - xOfs;

				int texel = PatternBuffer[(patternNo * 64) + (yOfs * 8) + (xOfs)];
				if (texel == 0) continue;
				int pixel = Palette[(palette * 16) + texel];
				FrameBuffer[(ScanLine * FrameWidth) + x] = pixel;
				PriorityBuffer[x] = (byte)(priority ? highPriority : lowPriority);
			}
		}

		void CalculateWindowScanlines(out int startScanline, out int endScanline)
		{
			int data = Registers[0x12];
			int windowVPosition = data & 31;
			bool fromTop = (data & 0x80) == 0;

			if (windowVPosition == 0)
			{
				startScanline = -1;
				endScanline = -1;
				return;
			}

			if (fromTop)
			{
				startScanline = 0;
				endScanline = (windowVPosition * 8);
			}
			else
			{
				startScanline = windowVPosition * 8;
				endScanline = FrameHeight;
			}
		}

		void CalculateWindowPosition(out int startPixel, out int endPixel)
		{
			int data = Registers[0x11];
			int windowHPosition = (data & 31) * 2; // Window H position is set in 2-cell increments
			bool fromLeft = (data & 0x80) == 0;

			if (windowHPosition == 0)
			{
				startPixel = -1;
				endPixel = -1;
				return;
			}

			if (fromLeft)
			{
				startPixel = 0;
				endPixel = (windowHPosition * 8);
				if (endPixel > FrameWidth)
					endPixel = FrameWidth;
			}
			else
			{
				startPixel = windowHPosition * 8;
				endPixel = FrameWidth;
				if (startPixel > FrameWidth)
				{
					startPixel = -1;
					endPixel = -1;
				}
			}
		}

		void RenderScrollA()
		{
			// Calculate scroll offsets

			int hscroll = CalcHScrollPlaneA(ScanLine);
			int vscroll = VSRAM[0] & 0x3FF;

			// Calculate window dimensions

			int startWindowScanline, endWindowScanline;
			int startWindowPixel, endWindowPixel;
			CalculateWindowScanlines(out startWindowScanline, out endWindowScanline);
			CalculateWindowPosition(out startWindowPixel, out endWindowPixel);

			// Render scanline

			if (ScanLine >= startWindowScanline && ScanLine < endWindowScanline)  // Window takes up whole scanline
			{
				RenderScrollAScanline(0, 0, NameTableAddrWindow, 0, FrameWidth, true);
			}
			else if (startWindowPixel != -1) // Window takes up partial scanline
			{
				if (startWindowPixel == 0) // Window grows from left side
				{
					RenderScrollAScanline(0, 0, NameTableAddrWindow, 0, endWindowPixel, true);
					RenderScrollAScanline(hscroll, vscroll, NameTableAddrA, endWindowPixel, FrameWidth, false);
				}
				else // Window grows from right side
				{
					RenderScrollAScanline(hscroll, vscroll, NameTableAddrA, 0, startWindowPixel, false);
					RenderScrollAScanline(0, 0, NameTableAddrWindow, startWindowPixel, FrameWidth, true);
				}
			}
			else // No window this scanline
			{
				RenderScrollAScanline(hscroll, vscroll, NameTableAddrA, 0, FrameWidth, false);
			}
		}

		void RenderScrollATwoCellVScroll()
		{
			// Calculate scroll offsets

			int hscroll = CalcHScrollPlaneA(ScanLine);

			// Calculate window dimensions

			int startWindowScanline, endWindowScanline;
			int startWindowPixel, endWindowPixel;
			CalculateWindowScanlines(out startWindowScanline, out endWindowScanline);
			CalculateWindowPosition(out startWindowPixel, out endWindowPixel);

			// Render scanline

			if (ScanLine >= startWindowScanline && ScanLine < endWindowScanline)  // Window takes up whole scanline
			{
				RenderScrollAScanline(0, 0, NameTableAddrWindow, 0, FrameWidth, true);
			}
			else if (startWindowPixel != -1) // Window takes up partial scanline
			{
				if (startWindowPixel == 0) // Window grows from left side
				{
					RenderScrollAScanline(0, 0, NameTableAddrWindow, 0, endWindowPixel, true);
					RenderScrollAScanlineTwoCellVScroll(hscroll, NameTableAddrA, endWindowPixel, FrameWidth, false);
				}
				else // Window grows from right side
				{
					RenderScrollAScanlineTwoCellVScroll(hscroll, NameTableAddrA, 0, startWindowPixel, false);
					RenderScrollAScanline(0, 0, NameTableAddrWindow, startWindowPixel, FrameWidth, true);
				}
			}
			else // No window this scanline
			{
				RenderScrollAScanlineTwoCellVScroll(hscroll, NameTableAddrA, 0, FrameWidth, false);
			}
		}

		void RenderScrollB()
		{
			int bgColor = BackgroundColor;
			int xScroll = CalcHScrollPlaneB(ScanLine);
			int yScroll = VSRAM[1] & 0x3FF;

			const int lowPriority = 1;
			const int highPriority = 4;

			int yTile = ((ScanLine + yScroll) / 8) % NameTableHeight;

			// this is hellllla slow. but not optimizing until we implement & understand
			// all scrolling modes, shadow & hilight, etc.
			// in thinking about this, you could convince me to optimize the PCE background renderer now.
			// Its way simple in comparison. But the PCE sprite renderer is way worse than gen.
			for (int x = 0; x < FrameWidth; x++)
			{
				int xTile = Math.Abs(((x + (1024 - xScroll)) / 8) % NameTableWidth);
				int xOfs = Math.Abs((x + (1024 - xScroll)) & 7);
				int yOfs = (ScanLine + yScroll) % 8;
				int cellOfs = NameTableAddrB + (yTile * NameTableWidth * 2) + (xTile * 2);
				int nameTableEntry = VRAM[cellOfs] | (VRAM[cellOfs + 1] << 8);
				int patternNo = nameTableEntry & 0x7FF;
				bool hFlip = ((nameTableEntry >> 11) & 1) != 0;
				bool vFlip = ((nameTableEntry >> 12) & 1) != 0;
				bool priority = ((nameTableEntry >> 15) & 1) != 0;
				int palette = (nameTableEntry >> 13) & 3;

				if (priority && PriorityBuffer[x] >= highPriority) continue;
				if (!priority && PriorityBuffer[x] >= lowPriority) continue;

				if (vFlip) yOfs = 7 - yOfs;
				if (hFlip) xOfs = 7 - xOfs;

				int texel = PatternBuffer[(patternNo * 64) + (yOfs * 8) + (xOfs)];
				int pixel = Palette[(palette * 16) + texel];
				if (texel != 0)
				{
					FrameBuffer[(ScanLine * FrameWidth) + x] = pixel;
					PriorityBuffer[x] = (byte)(priority ? highPriority : lowPriority);
				}
				else
				{
					FrameBuffer[(ScanLine * FrameWidth) + x] = bgColor;
				}
			}
		}

		void RenderScrollBTwoCellVScroll()
		{
			int bgColor = BackgroundColor;
			int xScroll = CalcHScrollPlaneB(ScanLine);
			int fineHScroll = xScroll & 15;

			const int lowPriority = 1;
			const int highPriority = 4;

			for (int x = 0; x < FrameWidth; x++)
			{
				int vsramUnitOffset = ((x - fineHScroll) / 16) % 40;
				int yScroll = VSRAM[(vsramUnitOffset * 2) + 1] & 0x3FF;
				int yTile = ((ScanLine + yScroll) / 8) % NameTableHeight;

				int xTile = Math.Abs(((x + (1024 - xScroll)) / 8) % NameTableWidth);
				int xOfs = Math.Abs((x + (1024 - xScroll)) & 7);
				int yOfs = (ScanLine + yScroll) % 8;
				int cellOfs = NameTableAddrB + (yTile * NameTableWidth * 2) + (xTile * 2);
				int nameTableEntry = VRAM[cellOfs] | (VRAM[cellOfs + 1] << 8);
				int patternNo = nameTableEntry & 0x7FF;
				bool hFlip = ((nameTableEntry >> 11) & 1) != 0;
				bool vFlip = ((nameTableEntry >> 12) & 1) != 0;
				bool priority = ((nameTableEntry >> 15) & 1) != 0;
				int palette = (nameTableEntry >> 13) & 3;

				if (priority && PriorityBuffer[x] >= highPriority) continue;
				if (!priority && PriorityBuffer[x] >= lowPriority) continue;

				if (vFlip) yOfs = 7 - yOfs;
				if (hFlip) xOfs = 7 - xOfs;

				int texel = PatternBuffer[(patternNo * 64) + (yOfs * 8) + (xOfs)];
				int pixel = Palette[(palette * 16) + texel];
				if (texel != 0)
				{
					FrameBuffer[(ScanLine * FrameWidth) + x] = pixel;
					PriorityBuffer[x] = (byte)(priority ? highPriority : lowPriority);
				}
				else
				{
					FrameBuffer[(ScanLine * FrameWidth) + x] = bgColor;
				}
			}
		}

		static readonly int[] SpriteSizeTable = { 8, 16, 24, 32 };
		Sprite sprite;

		void RenderSpritesScanline()
		{
			int scanLineBase = ScanLine * FrameWidth;
			int processedSprites = 0;
			int processedSpritesThisLine = 0;
			int processedDotsThisLine = 0;
			bool spriteMaskPrecursor = false;

			// This is incredibly unoptimized. TODO...

			FetchSprite(0);
			while (true)
			{
				if (sprite.Y > ScanLine || sprite.Y + sprite.HeightPixels <= ScanLine)
					goto nextSprite;

				processedSpritesThisLine++;
				processedDotsThisLine += sprite.WidthPixels;

				if (sprite.X > -128)
					spriteMaskPrecursor = true;

				if (sprite.X == -128 && spriteMaskPrecursor)
					break; // apply sprite mask

				if (sprite.X + sprite.WidthPixels <= 0)
					goto nextSprite;

				if (sprite.HeightCells == 2)
					sprite.HeightCells = 2;

				int yline = ScanLine - sprite.Y;
				if (sprite.VFlip)
					yline = sprite.HeightPixels - 1 - yline;
				int paletteBase = sprite.Palette * 16;
				if (sprite.HFlip == false)
				{
					int pattern = sprite.PatternIndex + ((yline / 8));

					for (int xi = 0; xi < sprite.WidthPixels; xi++)
					{
						if (sprite.X + xi < 0 || sprite.X + xi >= FrameWidth)
							continue;

						if (sprite.Priority == false && PriorityBuffer[sprite.X + xi] >= 3) continue;
						if (PriorityBuffer[sprite.X + xi] == 9) continue;

						int pixel = PatternBuffer[((pattern + ((xi / 8) * sprite.HeightCells)) * 64) + ((yline & 7) * 8) + (xi & 7)];
						if (pixel != 0)
						{
							FrameBuffer[scanLineBase + sprite.X + xi] = Palette[paletteBase + pixel];
							PriorityBuffer[sprite.X + xi] = 9;
						}
					}
				}
				else
				{ // HFlip
					int pattern = sprite.PatternIndex + ((yline / 8)) + (sprite.HeightCells * (sprite.WidthCells - 1));

					for (int xi = 0; xi < sprite.WidthPixels; xi++)
					{
						if (sprite.X + xi < 0 || sprite.X + xi >= FrameWidth)
							continue;

						if (sprite.Priority == false && PriorityBuffer[sprite.X + xi] >= 3) continue;
						if (PriorityBuffer[sprite.X + xi] == 9) continue;

						int pixel = PatternBuffer[((pattern + ((-xi / 8) * sprite.HeightCells)) * 64) + ((yline & 7) * 8) + (7 - (xi & 7))];
						if (pixel != 0)
						{
							FrameBuffer[scanLineBase + sprite.X + xi] = Palette[paletteBase + pixel];
							PriorityBuffer[sprite.X + xi] = 9;
						}
					}
				}

			nextSprite:
				if (sprite.Link == 0)
					break;
				if (++processedSprites >= SpriteLimit)
					break;
				if (processedSpritesThisLine >= SpritePerLineLimit)
					break;
				if (processedDotsThisLine >= DotsPerLineLimit)
					break;
				if (DisplayWidth == 32 && sprite.Link >= 64)
					break;
				FetchSprite(sprite.Link);
			}
		}

		void FetchSprite(int spriteNo)
		{
			// Note - X/Y coordinates are 10-bits (3FF) but must be masked to 9-bits (1FF)
			// In interlace mode this behavior should change

			int SatBase = SpriteAttributeTableAddr + (spriteNo * 8);
			sprite.Y = (VRAM[SatBase + 0] | (VRAM[SatBase + 1] << 8) & 0x1FF) - 128;
			sprite.X = (VRAM[SatBase + 6] | (VRAM[SatBase + 7] << 8) & 0x1FF) - 128;
			sprite.WidthPixels = SpriteSizeTable[(VRAM[SatBase + 3] >> 2) & 3];
			sprite.HeightPixels = SpriteSizeTable[VRAM[SatBase + 3] & 3];
			sprite.WidthCells = ((VRAM[SatBase + 3] >> 2) & 3) + 1;
			sprite.HeightCells = (VRAM[SatBase + 3] & 3) + 1;
			sprite.Link = VRAM[SatBase + 2] & 0x7F;
			sprite.PatternIndex = (VRAM[SatBase + 4] | (VRAM[SatBase + 5] << 8)) & 0x7FF;
			sprite.HFlip = ((VRAM[SatBase + 5] >> 3) & 1) != 0;
			sprite.VFlip = ((VRAM[SatBase + 5] >> 4) & 1) != 0;
			sprite.Palette = (VRAM[SatBase + 5] >> 5) & 3;
			sprite.Priority = ((VRAM[SatBase + 5] >> 7) & 1) != 0;
		}

		struct Sprite
		{
			public int X, Y;
			public int WidthPixels, HeightPixels;
			public int WidthCells, HeightCells;
			public int Link;
			public int Palette;
			public int PatternIndex;
			public bool Priority;
			public bool HFlip;
			public bool VFlip;
		}

		int CalcHScrollPlaneA(int line)
		{
			int ofs = 0;
			switch (Registers[11] & 3)
			{
				case 0: ofs = HScrollTableAddr; break;
				case 1: ofs = HScrollTableAddr + ((line & 7) * 4); break;
				case 2: ofs = HScrollTableAddr + ((line & ~7) * 4); break;
				case 3: ofs = HScrollTableAddr + (line * 4); break;
			}

			int value = VRAM[ofs] | (VRAM[ofs + 1] << 8);
			return value & 0x3FF;
		}

		int CalcHScrollPlaneB(int line)
		{
			int ofs = 0;
			switch (Registers[11] & 3)
			{
				case 0: ofs = HScrollTableAddr; break;
				case 1: ofs = HScrollTableAddr + ((line & 7) * 4); break;
				case 2: ofs = HScrollTableAddr + ((line & ~7) * 4); break;
				case 3: ofs = HScrollTableAddr + (line * 4); break;
			}

			int value = VRAM[ofs + 2] | (VRAM[ofs + 3] << 8);
			return value & 0x3FF;
		}
	}
}