using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Runtime.InteropServices;
using CdgLib.SubCode;

namespace CdgLib
{
    public class Graphic
    {
        private const int ColourTableSize = 16;
        private const int TileHeight = 12;
        private const int TileWidth = 6;
        public const int FullWidth = 300;
        public const int FullHeight = 216;

        private int _borderColourIndex;

        private int _horizonalOffset;
        private int _verticalOffset;

        private readonly int[] _colourTable = new int[ColourTableSize];
        private readonly byte[,] _pixelColours = new byte[FullHeight, FullWidth];
        private readonly int[,] _graphicData = new int[FullHeight, FullWidth];

        public Graphic(IEnumerable<Packet> packets)
        {
            foreach (var packet in packets)
            {
                Process(packet);
            }
        }

        public Bitmap ToBitmap()
        {
            RenderSurface();
            Bitmap myBitmap;
            using (var bitmapStream = new MemoryStream())
            {
                foreach (var colourValue in _graphicData)
                {
                    var colour = BitConverter.GetBytes(colourValue);
                    bitmapStream.Write(colour, 0, 4);
                }
                myBitmap = StreamToBitmap(bitmapStream, FullWidth, FullHeight);
            }
            myBitmap.MakeTransparent(myBitmap.GetPixel(1, 1));
    
            return myBitmap;
        }

        private Bitmap StreamToBitmap(Stream stream, int width, int height)
        {
            //create a new bitmap
            var bmp = new Bitmap(width, height, PixelFormat.Format32bppArgb);
            var bmpData = bmp.LockBits(new Rectangle(0, 0, width, height), ImageLockMode.WriteOnly, bmp.PixelFormat);
            stream.Seek(0, SeekOrigin.Begin);
            //copy the stream of pixel
            for (var n = 0; n <= stream.Length - 1; n++)
            {
                var myByte = new byte[1];
                stream.Read(myByte, 0, 1);
                Marshal.WriteByte(bmpData.Scan0, n, myByte[0]);
            }
            bmp.UnlockBits(bmpData);
            return bmp;
        }

        private void Process(Packet packet)
        {
            if (packet.Command != Command.Graphic) return;
            switch (packet.Instruction)
            {
                case Instruction.MemoryPreset:
                    MemoryPreset(packet);
                    break;
                case Instruction.BorderPreset:
                    BorderPreset(packet);
                    break;
                case Instruction.TileBlockNormal:
                    TileBlock(packet, false);
                    break;
                case Instruction.ScrollPreset:
                    Scroll(packet, false);
                    break;
                case Instruction.ScrollCopy:
                    Scroll(packet, true);
                    break;
                case Instruction.DefineTransparentColor:
                    DefineTransparentColour(packet);
                    break;
                case Instruction.LoadColorTableLower:
                    LoadColorTable(packet, 0);
                    break;
                case Instruction.LoadColorTableUpper:
                    LoadColorTable(packet, 1);
                    break;
                case Instruction.TileBlockXor:
                    TileBlock(packet, true);
                    break;
            }
        }


        private void MemoryPreset(Packet packet)
        {
            var colour = packet.Data[0] & 0xf;
            _borderColourIndex = colour;
            var repeat = packet.Data[1] & 0xf;

            //we have a reliable packet stream, so the repeat command 
            //is executed only the first time
            if (repeat == 0)
            {
                //Note that this may be done before any load colour table
                //commands by some CDGs. So the load colour table itself
                //actual recalculates the RGB values for all pixels when
                //the colour table changes.

                //Set the preset colour for every pixel. Must be stored in 
                //the pixel colour table indeces array
                for (int rowIndex = 0; rowIndex < _pixelColours.GetLength(0); rowIndex++)
                {
                    for (int columnIndex = 0; columnIndex < _pixelColours.GetLength(1); columnIndex++)
                    {
                        _pixelColours[rowIndex, columnIndex] = (byte)colour;
                    }
                }
            }
        }

        private void BorderPreset(Packet packet)
        {
            int rowIndex;
            int columnIndex;

            var colour = packet.Data[0] & 0xf;
            _borderColourIndex = colour;

            //The border area is the area contained with a rectangle 
            //defined by (0,0,300,216) minus the interior pixels which are contained
            //within a rectangle defined by (6,12,294,204).

            for (rowIndex = 0; rowIndex < _pixelColours.GetLength(0); rowIndex++)
            {
                for (columnIndex = 0; columnIndex < 6; columnIndex++)
                {
                    _pixelColours[rowIndex, columnIndex] = (byte)colour;
                }

                for (columnIndex = _pixelColours.GetLength(1) - 6; columnIndex < _pixelColours.GetLength(1); columnIndex++)
                {
                    _pixelColours[rowIndex, columnIndex] = (byte)colour;
                }
            }

            for (columnIndex = 6; columnIndex < _pixelColours.GetLength(1) - 6; columnIndex++)
            {
                for (rowIndex = 0; rowIndex < 12; rowIndex++)
                {
                    _pixelColours[rowIndex, columnIndex] = (byte)colour;
                }

                for (rowIndex = _pixelColours.GetLength(0) - 12; rowIndex < _pixelColours.GetLength(0); rowIndex++)
                {
                    _pixelColours[rowIndex, columnIndex] = (byte)colour;
                }
            }
        }


        private void LoadColorTable(Packet packet, int table)
        {
            for (var i = 0; i < 8; i++)
            {
                //[---high byte---]   [---low byte----]
                //7 6 5 4 3 2 1 0     7 6 5 4 3 2 1 0
                //X X r r r r g g     X X g g b b b b
                var highByte = packet.Data[2 * i];
                var lowByte = packet.Data[2 * i + 1];

                var red = (highByte & 0x3f) >> 2;
                var green = ((highByte & 0x3) << 2) | ((lowByte & 0x3f) >> 4);
                var blue = lowByte & 0xf;

                //4 bit colour to 8 bit colour
                red *= 17;
                green *= 17;
                blue *= 17;

                _colourTable[i + table*8] = Color.FromArgb(red, green, blue).ToArgb(); 
            }
        }


        private void TileBlock(Packet packet, bool bXor)
        {
            var colour0 = packet.Data[0] & 0xf;
            var colour1 = packet.Data[1] & 0xf;
            var rowIndex = (packet.Data[2] & 0x1f) * 12;
            var columnIndex = (packet.Data[3] & 0x3f) * 6;

            if (rowIndex > FullHeight - TileHeight)
                return;
            if (columnIndex > FullWidth - TileWidth)
                return;

            //Set the pixel array for each of the pixels in the 12x6 tile.
            //Normal = Set the colour to either colour0 or colour1 depending
            //on whether the pixel value is 0 or 1.
            //XOR = XOR the colour with the colour index currently there.


            for (var i = 0; i <= 11; i++)
            {
                var myByte = packet.Data[4 + i] & 0x3f;
                for (var j = 0; j <= 5; j++)
                {
                    var pixel = (myByte >> (5 - j)) & 0x1;
                    var newCol = 0;
                    if (bXor)
                    {
                        //Tile Block XOR 
                        var xorCol = pixel == 0 ? colour0 : colour1;

                        //Get the colour index currently at this location, and xor with it 
                        int currentColourIndex = _pixelColours[rowIndex + i, columnIndex + j];
                        newCol = currentColourIndex ^ xorCol;
                    }
                    else
                    {
                        newCol = pixel == 0 ? colour0 : colour1;
                    }

                    //Set the pixel with the new colour. We set both the surfarray
                    //containing actual RGB values, as well as our array containing
                    //the colour indexes into our colour table. 
                    _pixelColours[rowIndex + i, columnIndex + j] = (byte)newCol;
                }
            }
        }

        private void DefineTransparentColour(Packet packet)
        {
            //_mTransparentColour = packet.Data[0] & 0xf;
        }


        private void Scroll(Packet packet, bool copy)
        {
            //Decode the scroll command parameters
            var colour = packet.Data[0] & 0xf;
            var horizontalScroll = packet.Data[1] & 0x3f;
            var verticalScroll = packet.Data[2] & 0x3f;

            var horizontalScrollCommand = (horizontalScroll & 0x30) >> 4;
            var horizontalOffset = horizontalScroll & 0x7;
            var verticalScrollCommand = (verticalScroll & 0x30) >> 4;
            var verticalOffset = verticalScroll & 0xf;


            _horizonalOffset = horizontalOffset < 5 ? horizontalOffset : 5;
            this._verticalOffset = verticalOffset < 11 ? verticalOffset : 11;

            //Scroll Vertical - Calculate number of pixels

            var verticalScrollPixels = 0;
            switch (verticalScrollCommand)
            {
                case 2:
                    verticalScrollPixels = -12;
                    break;
                case 1:
                    verticalScrollPixels = 12;
                    break;
            }

            //Scroll Horizontal- Calculate number of pixels

            var horizontalScrollPixels = 0;
            switch (horizontalScrollCommand)
            {
                case 2:
                    horizontalScrollPixels = -6;
                    break;
                case 1:
                    horizontalScrollPixels = 6;
                    break;
            }

            if (horizontalScrollPixels == 0 && verticalScrollPixels == 0)
            {
                return;
            }

            //Perform the actual scroll.

            var temp = new byte[FullHeight + 1, FullWidth + 1];
            var vInc = verticalScrollPixels + FullHeight;
            var hInc = horizontalScrollPixels + FullWidth;
            var rowIndex = 0;
            //row index
            var columnIndex = 0;
            //column index

            for (rowIndex = 0; rowIndex <= FullHeight - 1; rowIndex++)
            {
                for (columnIndex = 0; columnIndex <= FullWidth - 1; columnIndex++)
                {
                    temp[(rowIndex + vInc) % FullHeight, (columnIndex + hInc) % FullWidth] = _pixelColours[rowIndex, columnIndex];
                }
            }


            //if copy is false, we were supposed to fill in the new pixels
            //with a new colour. Go back and do that now.


            if (copy == false)
            {
                if (verticalScrollPixels > 0)
                {
                    for (columnIndex = 0; columnIndex <= FullWidth - 1; columnIndex++)
                    {
                        for (rowIndex = 0; rowIndex <= verticalScrollPixels - 1; rowIndex++)
                        {
                            temp[rowIndex, columnIndex] = (byte)colour;
                        }
                    }
                }
                else if (verticalScrollPixels < 0)
                {
                    for (columnIndex = 0; columnIndex <= FullWidth - 1; columnIndex++)
                    {
                        for (rowIndex = FullHeight + verticalScrollPixels; rowIndex <= FullHeight - 1; rowIndex++)
                        {
                            temp[rowIndex, columnIndex] = (byte)colour;
                        }
                    }
                }


                if (horizontalScrollPixels > 0)
                {
                    for (columnIndex = 0; columnIndex <= horizontalScrollPixels - 1; columnIndex++)
                    {
                        for (rowIndex = 0; rowIndex <= FullHeight - 1; rowIndex++)
                        {
                            temp[rowIndex, columnIndex] = (byte)colour;
                        }
                    }
                }
                else if (horizontalScrollPixels < 0)
                {
                    for (columnIndex = FullWidth + horizontalScrollPixels; columnIndex <= FullWidth - 1; columnIndex++)
                    {
                        for (rowIndex = 0; rowIndex <= FullHeight - 1; rowIndex++)
                        {
                            temp[rowIndex, columnIndex] = (byte)colour;
                        }
                    }
                }
            }

            //Now copy the temporary buffer back to our array

            for (rowIndex = 0; rowIndex <= FullHeight - 1; rowIndex++)
            {
                for (columnIndex = 0; columnIndex <= FullWidth - 1; columnIndex++)
                {
                    _pixelColours[rowIndex, columnIndex] = temp[rowIndex, columnIndex];
                }
            }
        }

        private void RenderSurface()
        {
            for (var rowIndex = 0; rowIndex <= FullHeight - 1; rowIndex++)
            {
                for (var columnIndex = 0; columnIndex <= FullWidth - 1; columnIndex++)
                {
                    if (rowIndex < TileHeight || rowIndex >= FullHeight - TileHeight || columnIndex < TileWidth ||
                        columnIndex >= FullWidth - TileWidth)
                    {
                        _graphicData[rowIndex, columnIndex] = _colourTable[_borderColourIndex];
                    }
                    else
                    {
                        _graphicData[rowIndex, columnIndex] = _colourTable[_pixelColours[rowIndex + _verticalOffset, columnIndex + _horizonalOffset]];
                    }
                }
            }
        }
    }
}
