﻿using System;

namespace Oni.Imaging
{
    internal class Surface
    {
        private int width;
        private int height;
        private int stride;
        private int pixelSize;
        private SurfaceFormat format;
        private byte[] data;

        public Surface(int width, int height)
            : this(width, height, SurfaceFormat.RGBA)
        {
        }

        public Surface(int width, int height, SurfaceFormat format)
        {
            if (format == SurfaceFormat.DXT1)
            {
                width = Math.Max(width, 4);
                height = Math.Max(height, 4);
            }

            this.width = width;
            this.height = height;
            this.format = format;

            pixelSize = GetPixelSize(format);
            stride = pixelSize * width;
            data = new byte[GetDataSize(width, height, format)];
        }

        public Surface(int width, int height, SurfaceFormat format, byte[] data)
        {
            if (format == SurfaceFormat.DXT1)
            {
                width = Math.Max(width, 4);
                height = Math.Max(height, 4);
            }

            this.width = width;
            this.height = height;
            this.format = format;
            this.data = data;

            pixelSize = GetPixelSize(format);
            stride = pixelSize * width;
        }

        private static int GetDataSize(int width, int height, SurfaceFormat format)
        {
            switch (format)
            {
                case SurfaceFormat.BGRA4444:
                case SurfaceFormat.BGRX5551:
                case SurfaceFormat.BGRA5551:
                    return width * height * 2;

                case SurfaceFormat.BGRX:
                case SurfaceFormat.BGRA:
                case SurfaceFormat.RGBX:
                case SurfaceFormat.RGBA:
                    return width * height * 4;

                case SurfaceFormat.DXT1:
                    return width * height / 2;

                default:
                    throw new NotSupportedException(string.Format("Unsupported texture format {0}", format));
            }
        }

        private static int GetPixelSize(SurfaceFormat format)
        {
            switch (format)
            {
                case SurfaceFormat.BGRA4444:
                case SurfaceFormat.BGRX5551:
                case SurfaceFormat.BGRA5551:
                    return 2;

                case SurfaceFormat.BGRX:
                case SurfaceFormat.BGRA:
                case SurfaceFormat.RGBX:
                case SurfaceFormat.RGBA:
                    return 4;

                case SurfaceFormat.DXT1:
                    return 2;

                default:
                    throw new NotSupportedException(string.Format("Unsupported texture format {0}", format));
            }
        }

        public int Width => width;
        public int Height => height;
        public SurfaceFormat Format => format;
        public byte[] Data => data;

        public bool HasAlpha
        {
            get
            {
                switch (format)
                {
                    case SurfaceFormat.BGRA:
                    case SurfaceFormat.RGBA:
                    case SurfaceFormat.BGRA4444:
                    case SurfaceFormat.BGRA5551:
                        return true;

                    default:
                        return false;
                }
            }
        }

        public void CleanupAlpha()
        {
            if (format != SurfaceFormat.BGRA5551 && format != SurfaceFormat.RGBA && format != SurfaceFormat.BGRA)
                return;

            if (!HasTransparentPixels())
            {
                switch (format)
                {
                    case SurfaceFormat.BGRA5551:
                        format = SurfaceFormat.BGRX5551;
                        break;
                    case SurfaceFormat.BGRA:
                        format = SurfaceFormat.BGRX;
                        break;
                    case SurfaceFormat.RGBA:
                        format = SurfaceFormat.RGBX;
                        break;
                }
            }
        }

        public bool HasTransparentPixels()
        {
            for (int y = 0; y < height; y++)
            {
                for (int x = 0; x < width; x++)
                {
                    Color c = GetPixel(x, y);

                    if (c.A != 255)
                        return true;
                }
            }

            return false;
        }

        public Color this[int x, int y]
        {
            get
            {
                if (x < 0 || width <= x || y < 0 || height <= y)
                    return Color.Black;

                return GetPixel(x, y);
            }
            set
            {
                if (x < 0 || width <= x || y < 0 || height <= y)
                    return;

                SetPixel(x, y, value);
            }
        }

        public void FlipVertical()
        {
            var temp = new byte[stride];

            for (int y = 0; y < height / 2; y++)
            {
                int ry = height - y - 1;

                Array.Copy(data, y * stride, temp, 0, stride);
                Array.Copy(data, ry * stride, data, y * stride, stride);
                Array.Copy(temp, 0, data, ry * stride, stride);
            }
        }

        public void FlipHorizontal()
        {
            for (int y = 0; y < height; y++)
            {
                for (int x = 0; x < width / 2; x++)
                {
                    int rx = width - x - 1;

                    Color c1 = GetPixel(x, y);
                    Color c2 = GetPixel(rx, y);
                    SetPixel(x, y, c2);
                    SetPixel(rx, y, c1);
                }
            }
        }

        public void Rotate90()
        {
            for (int x = 0; x < width; x++)
            {
                for (int y = 0; y < height; y++)
                {
                    if (x <= y)
                        continue;

                    var c1 = GetPixel(x, y);
                    var c2 = GetPixel(y, x);
                    SetPixel(x, y, c2);
                    SetPixel(y, x, c1);
                }
            }
        }

        public Surface Convert(SurfaceFormat dstFormat)
        {
            Surface dst;

            if (format == dstFormat)
            {
                dst = new Surface(width, height, dstFormat, (byte[])data.Clone());
            }
            else if (dstFormat == SurfaceFormat.DXT1)
            {
                dst = Dxt1.Compress(this);
            }
            else if (format == SurfaceFormat.DXT1)
            {
                dst = Dxt1.Decompress(this, dstFormat);
            }
            else
            {
                dst = new Surface(width, height, dstFormat);

                for (int y = 0; y < height; y++)
                {
                    for (int x = 0; x < width; x++)
                        dst.SetPixel(x, y, GetPixel(x, y));
                }
            }

            return dst;
        }

        public Surface Resize(int newWidth, int newHeight)
        {
            if (newWidth > width || newHeight > height)
                throw new NotImplementedException();

            var dst = new Surface(newWidth, newHeight, format);

            if (newWidth * 2 == width && newHeight * 2 == height)
            {
                Halfsize(dst);
                return dst;
            }

            float sx = (float)width / dst.width;
            float sy = (float)height / dst.height;

            for (int dstY = 0; dstY < dst.height; dstY++)
            {
                float top = dstY * sy;
                float bottom = top + sy;

                int yTop = (int)top;
                int yBottom = (int)(bottom - 0.001f);

                float topWeight = 1.0f - (top - yTop);
                float bottomWeight = bottom - yBottom;

                for (int dstX = 0; dstX < dst.width; dstX++)
                {
                    float left = dstX * sx;
                    float right = left + sx;

                    int xLeft = (int)left;
                    int xRight = (int)(right - 0.001f);

                    float leftWeight = 1.0f - (left - xLeft);
                    float rightWeight = right - xRight;

                    var sum = GetVector4(xLeft, yTop) * (leftWeight * topWeight);
                    sum += GetVector4(xRight, yTop) * (rightWeight * topWeight);
                    sum += GetVector4(xLeft, yBottom) * (leftWeight * bottomWeight);
                    sum += GetVector4(xRight, yBottom) * (rightWeight * bottomWeight);

                    for (int y = yTop + 1; y < yBottom; y++)
                    {
                        sum += GetVector4(xLeft, y) * leftWeight;
                        sum += GetVector4(xRight, y) * rightWeight;
                    }

                    for (int x = xLeft + 1; x < xRight; x++)
                    {
                        sum += GetVector4(x, yTop) * topWeight;
                        sum += GetVector4(x, yBottom) * bottomWeight;
                    }

                    for (int y = yTop + 1; y < yBottom; y++)
                    {
                        for (int x = xLeft + 1; x < xRight; x++)
                            sum += GetVector4(x, y);
                    }

                    float area = (right - left) * (bottom - top);

                    dst.SetPixel(dstX, dstY, new Color(sum / area));
                }
            }

            return dst;
        }

        private void Halfsize(Surface dst)
        {
            int halfWidth = dst.width;
            int halfHeight = dst.height;

            for (int dstY = 0; dstY < halfHeight; dstY++)
            {
                int yTop = dstY * 2;
                int yBottom = yTop + 1;

                for (int dstX = 0; dstX < halfWidth; dstX++)
                {
                    int xLeft = dstX * 2;
                    int xRight = xLeft + 1;

                    var sum = GetVector4(xLeft, yTop);
                    sum += GetVector4(xRight, yTop);
                    sum += GetVector4(xLeft, yBottom);
                    sum += GetVector4(xRight, yBottom);

                    dst.SetPixel(dstX, dstY, new Color(sum / 4.0f));
                }
            }
        }

        private Vector4 GetVector4(int x, int y)
        {
            return GetPixel(x, y).ToVector4();
        }

        private Color GetPixel(int x, int y)
        {
            int i = x * pixelSize + y * stride;

            switch (format)
            {
                case SurfaceFormat.BGRA4444:
                    return Color.ReadBgra4444(data, i);
                case SurfaceFormat.BGRX5551:
                    return Color.ReadBgrx5551(data, i);
                case SurfaceFormat.BGRA5551:
                    return Color.ReadBgra5551(data, i);
                case SurfaceFormat.BGR565:
                    return Color.ReadBgr565(data, i);
                case SurfaceFormat.BGRX:
                    return Color.ReadBgrx(data, i);
                case SurfaceFormat.BGRA:
                    return Color.ReadBgra(data, i);
                case SurfaceFormat.RGBX:
                    return Color.ReadRgbx(data, i);
                case SurfaceFormat.RGBA:
                    return Color.ReadRgba(data, i);
                default:
                    throw new NotSupportedException(string.Format("Unsupported texture format {0}", format));
            }
        }

        private void SetPixel(int x, int y, Color color)
        {
            int i = x * pixelSize + y * stride;

            switch (format)
            {
                case SurfaceFormat.BGRA4444:
                    Color.WriteBgra4444(color, data, i);
                    return;
                case SurfaceFormat.BGRX5551:
                    Color.WriteBgrx5551(color, data, i);
                    return;
                case SurfaceFormat.BGRA5551:
                    Color.WriteBgra5551(color, data, i);
                    return;
                case SurfaceFormat.BGR565:
                    Color.WriteBgr565(color, data, i);
                    return;
                case SurfaceFormat.BGRX:
                    Color.WriteBgrx(color, data, i);
                    return;
                case SurfaceFormat.BGRA:
                    Color.WriteBgra(color, data, i);
                    return;
                case SurfaceFormat.RGBX:
                    Color.WriteRgbx(color, data, i);
                    return;
                case SurfaceFormat.RGBA:
                    Color.WriteRgba(color, data, i);
                    return;
                default:
                    throw new NotSupportedException(string.Format("Unsupported texture format {0}", format));
            }
        }

        public void Fill(int x, int y, int width, int height, Color color)
        {
            for (int px = x; px < x + width; px++)
            {
                for (int py = y; py < y + height; py++)
                    SetPixel(px, py, color);
            }
        }
    }
}
