using LibDgf.Txm;
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;

namespace LibDgf.Ps2.Gs
{
    public static class GsMemoryUtils
    {
        public const int BYTES_PER_COLUMN = 64;
        public const int COLUMNS_PER_BLOCK = 4;
        public const int BYTES_PER_BLOCK = BYTES_PER_COLUMN * COLUMNS_PER_BLOCK;
        public const int BLOCKS_PER_PAGE = 32;
        public const int BYTES_PER_PAGE = BYTES_PER_BLOCK * BLOCKS_PER_PAGE;
        public const int TOTAL_PAGES = 512;
        public const int TOTAL_BYTES = BYTES_PER_PAGE * TOTAL_PAGES;

        public class ColumnParams
        {
            public int Width { get; }
            public int Height { get; }
            public int BitsPerPixel { get; }
            public int ColsPerPage { get; }
            public int RowsPerPage { get; }

            internal ColumnParams(int width, int height, int bitsPerPixel, int colsPerPage, int rowsPerPage)
            {
                Width = width;
                Height = height;
                BitsPerPixel = bitsPerPixel;
                ColsPerPage = colsPerPage;
                RowsPerPage = rowsPerPage;
            }
        }

        static readonly ColumnParams PSMCT32_COLUMN_PARAMS = new ColumnParams(8, 2, 32, 8, 4);
        static readonly ColumnParams PSMT8_COLUMN_PARAMS = new ColumnParams(16, 4, 8, 8, 4);
        static readonly ColumnParams PSMT4_COLUMN_PARAMS = new ColumnParams(32, 4, 4, 4, 8);

        public static ColumnParams GetColumnParams(TxmPixelFormat format)
        {
            switch (format)
            {
                case TxmPixelFormat.PSMCT32:
                case TxmPixelFormat.PSMCT24:
                    return PSMCT32_COLUMN_PARAMS;
                case TxmPixelFormat.PSMT8:
                    return PSMT8_COLUMN_PARAMS;
                case TxmPixelFormat.PSMT4:
                    return PSMT4_COLUMN_PARAMS;
                default:
                    throw new NotSupportedException($"{format} not supported.");
            }
        }

        public static byte[] ReadColumn(BinaryReader br, TxmPixelFormat format, int bytesPerLine)
        {
            ColumnParams colParams = GetColumnParams(format);
            byte[] data = new byte[colParams.Width * colParams.Height * colParams.BitsPerPixel / 8];
            int bytesPerDestLine = colParams.Width * colParams.BitsPerPixel / 8;
            int skipLength = bytesPerLine - bytesPerDestLine;
            for (int i = 0; i < colParams.Height; ++i)
            {
                byte[] lineData = br.ReadBytes(bytesPerDestLine);
                br.BaseStream.Seek(skipLength, SeekOrigin.Current);
                Buffer.BlockCopy(lineData, 0, data, i * bytesPerDestLine, lineData.Length);
            }
            return data;
        }

        public static void WriteColumn(BinaryWriter bw, TxmPixelFormat format, int bytesPerLine, byte[] data)
        {
            ColumnParams colParams = GetColumnParams(format);
            int bytesPerDestLine = colParams.Width * colParams.BitsPerPixel / 8;
            int skipLength = bytesPerLine - bytesPerDestLine;
            for (int i = 0; i < colParams.Height; ++i)
            {
                bw.Write(data, i * bytesPerDestLine, bytesPerDestLine);
                bw.Seek(skipLength, SeekOrigin.Current);
            }
        }

        // --------------------------------------------------------------------

        // Block address to linear offset lookup for PSMCT32
        static readonly int[,] PSMCT32_BLOCK_LOOKUP = new[,]
        {
            { 0, 1, 4, 5, 16, 17, 20, 21 },
            { 2, 3, 6, 7, 18, 19, 22, 23 },
            { 8, 9, 12, 13, 24, 25, 28, 29 },
            { 10, 11, 14, 15, 26, 27, 30, 31 }
        };

        static readonly int[,] PSMT8_BLOCK_LOOKUP = new[,]
        {
            { 0, 1, 4, 5, 16, 17, 20, 21 },
            { 2, 3, 6, 7, 18, 19, 22, 23 },
            { 8, 9, 12, 13, 24, 25, 28, 29 },
            { 10, 11, 14, 15, 26, 27, 30, 31 }
        };

        static readonly int[,] PSMT4_BLOCK_LOOKUP = new[,]
        {
            { 0, 2, 8, 10 },
            { 1, 3, 9, 11 },
            { 4, 6, 12, 14 },
            { 5, 7, 13, 15 },
            { 16, 18, 24, 26 },
            { 17, 19, 25, 27 },
            { 20, 22, 28, 30 },
            { 21, 23, 29, 31 }
        };

        static readonly int[] PSMCT32_BLOCK_REVERSE_LOOKUP;
        static readonly int[] PSMT8_BLOCK_REVERSE_LOOKUP;
        static readonly int[] PSMT4_BLOCK_REVERSE_LOOKUP;

        static GsMemoryUtils()
        {
            PSMCT32_BLOCK_REVERSE_LOOKUP = MakeReverseLookup(PSMCT32_BLOCK_LOOKUP);
            PSMT8_BLOCK_REVERSE_LOOKUP = MakeReverseLookup(PSMT8_BLOCK_LOOKUP);
            PSMT4_BLOCK_REVERSE_LOOKUP = MakeReverseLookup(PSMT4_BLOCK_LOOKUP);
        }

        static int[] MakeReverseLookup(int[,] lut)
        {
            int[] reverse = new int[lut.Length];
            int i = 0;
            foreach (var n in lut)
            {
                reverse[n] = i++;
            }
            return reverse;
        }

        public static int CalcBlockNumber(TxmPixelFormat format, int blockX, int blockY, int texBufWidth)
        {
            ColumnParams colParams = GetColumnParams(format);
            switch (format)
            {
                case TxmPixelFormat.PSMCT32:
                    return CalcBlockNumber(colParams, PSMCT32_BLOCK_LOOKUP,blockX, blockY, texBufWidth);
                case TxmPixelFormat.PSMT8:
                    return CalcBlockNumber(colParams, PSMT8_BLOCK_LOOKUP, blockX, blockY, texBufWidth);
                case TxmPixelFormat.PSMT4:
                    return CalcBlockNumber(colParams, PSMT4_BLOCK_LOOKUP, blockX, blockY, texBufWidth);
                default:
                    throw new NotSupportedException($"{format} not supported");
            }
        }

        public static int CalcBlockMemoryOffset(TxmPixelFormat format, int index)
        {
            ColumnParams colParams = GetColumnParams(format);
            switch (format)
            {
                case TxmPixelFormat.PSMCT32:
                    return CalcBlockMemoryOffset(colParams, PSMCT32_BLOCK_REVERSE_LOOKUP, index);
                case TxmPixelFormat.PSMT8:
                    return CalcBlockMemoryOffset(colParams, PSMT8_BLOCK_REVERSE_LOOKUP, index);
                case TxmPixelFormat.PSMT4:
                    return CalcBlockMemoryOffset(colParams, PSMT4_BLOCK_REVERSE_LOOKUP, index);
                default:
                    throw new NotSupportedException($"{format} not supported");
            }
        }

        static int CalcBlockNumber(ColumnParams colParams, int[,] lut, int blockX, int blockY, int texBufWidth)
        {
            int pageX = blockX / colParams.ColsPerPage;
            int pageY = blockY / colParams.RowsPerPage;
            int blockXInPage = blockX % colParams.ColsPerPage;
            int blockYInPage = blockY % colParams.RowsPerPage;

            return (pageY * texBufWidth + pageX) * BLOCKS_PER_PAGE + lut[blockYInPage, blockXInPage];
        }

        static int CalcBlockMemoryOffset(ColumnParams colParams, int[] lut, int index)
        {
            int pageIndex = index / BLOCKS_PER_PAGE;
            int rem = index % BLOCKS_PER_PAGE;
            int pixelsPerBlock = colParams.Width * colParams.Height * COLUMNS_PER_BLOCK;
            int memBlockNum = lut[rem];
            int fullBlockRows = memBlockNum / colParams.ColsPerPage;
            int partialBlocks = memBlockNum % colParams.ColsPerPage;

            return (
                pageIndex * BLOCKS_PER_PAGE * pixelsPerBlock + // Full pages
                fullBlockRows * pixelsPerBlock * colParams.ColsPerPage + // Full row of blocks
                partialBlocks * colParams.Width // Partial row of blocks
            ) * colParams.BitsPerPixel / 8;
        }

        public static int CalcTxmImageOffset(ColumnParams colParams, int blockIndex, int imageWidth)
        {
            int blocksPerRow = imageWidth / colParams.Width;
            if (blocksPerRow == 0) blocksPerRow = 1;
            int fullRows = blockIndex / blocksPerRow;
            int rowBlockIndex = blockIndex % blocksPerRow;
            return (
                fullRows * imageWidth * colParams.Height * 4 +
                rowBlockIndex * colParams.Width
            ) * colParams.BitsPerPixel / 8;
        }
    }
}