using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.Drawing.Text;
using System.IO;
using Sdl;
using StarEngine.Core;

namespace StarEngine.Sdl
{
    /// <summary>
    /// 画像そのものを表します。
    /// </summary>
    internal sealed class Texture : ITexture
    {
        internal Texture(Size size)
        {
            this.Size = size;
            this.Pixels = new uint[this.Width * this.Height];
        }

        internal Texture(Size size, uint[] pixels)
        {
            Debug.Assert(size.Width * size.Height == pixels.Length);

            this.Size = size;
            this.Pixels = (uint[])pixels.Clone();
        }

        private void FromBitmap(Bitmap bitmap)
        {
            this.Size = new Size(bitmap.Width, bitmap.Height);
            this.Pixels = new uint[this.Width * this.Height];

            BitmapData bitmapData = bitmap.LockBits(
                new Rectangle(Point.Empty, bitmap.Size),
                ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
            unsafe
            {
                uint[] pixels = this.Pixels;
                uint* p = (uint*)bitmapData.Scan0;
                for (int i = 0; i < pixels.Length; i++)
                    pixels[i] = *p++;
            }
            bitmap.UnlockBits(bitmapData);
        }

        internal Texture(string path)
        {
            using (Stream stream = FileSystem.Load(path))
            using (Bitmap bitmap = new Bitmap(stream))
                this.FromBitmap(bitmap);
        }

        /// <summary>
        /// ピクセルデータの配列を取得します。
        /// </summary>
        internal uint[] Pixels
        {
            get { return this.pixels; }
            private set { this.pixels = value; }
        }
        private uint[] pixels;

        public Size Size
        {
            get { return this.size; }
            private set { this.size = value; }
        }
        private Size size;

        public int Width
        {
            get { return this.Size.Width; }
        }

        public int Height
        {
            get { return this.Size.Height; }
        }

        object ICloneable.Clone()
        {
            return this.Clone();
        }

        public Texture Clone()
        {
            return new Texture(this.Size, this.Pixels);
        }

        public void Dispose()
        {
            this.IsDisposed = true;
            this.Pixels = null;
        }

        public bool IsDisposed
        {
            get { return this.isDisposed; }
            private set { this.isDisposed = value; }
        }
        private bool isDisposed = false;

        public Bitmap ToBitmap()
        {
            Bitmap bitmap =
                new Bitmap(this.Width, this.Height, PixelFormat.Format32bppArgb);
            BitmapData bitmapData = bitmap.LockBits(
                new Rectangle(Point.Empty, bitmap.Size),
                ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
            unsafe
            {
                uint[] srcP = this.Pixels;
                uint* dstP = (uint*)bitmapData.Scan0;
                for (int i = 0; i < srcP.Length; i++)
                    *dstP++ = 0xff000000 | srcP[i];
            }
            bitmap.UnlockBits(bitmapData);
            return bitmap;
        }

        public void ChangeHue(int dHue)
        {
            dHue %= 360;
            if (dHue == 0)
                return;

            uint[] pixels = this.Pixels;

            for (int i = 0; i < pixels.Length; i++)
            {
                uint c = pixels[i];
                byte a = (byte)(c >> 24);
                byte r = (byte)(c >> 16);
                byte g = (byte)(c >> 8);
                byte b = (byte)c;

                byte max = Math.Max(r, Math.Max(g, b));
                byte min = Math.Min(r, Math.Min(g, b));
                if (max == 0) continue;

                double h, s, v;
                double delta255 = max - min;
                double delta = delta255 / 255.0;
                v = max / 255.0;
                s = delta255 / max;
                if (max == r)
                    h = (g - b) / delta255;
                else if (max == g)
                    h = 2 + (b - r) / delta255;
                else
                    h = 4 + (r - g) / delta255;

                if (h < 0.0)
                    h += 6.0;
                h += dHue * 6.0 / 360.0;
                if (6.0 <= h)
                    h -= 6.0;

                int ii = (int)Math.Floor(h);
                double f = h - ii;
                double aa = v * (1 - s);
                double bb = v * (1 - s * f);
                double cc = v * (1 - s * (1 - f));
                byte v255 = (byte)(v * 255);
                byte aa255 = (byte)(aa * 255);
                byte bb255 = (byte)(bb * 255);
                byte cc255 = (byte)(cc * 255);
                switch (ii)
                {
                case 0:
                    r = v255;
                    g = cc255;
                    b = aa255;
                    break;
                case 1:
                    r = bb255;
                    g = v255;
                    b = aa255;
                    break;
                case 2:
                    r = aa255;
                    g = v255;
                    b = cc255;
                    break;
                case 3:
                    r = aa255;
                    g = bb255;
                    b = v255;
                    break;
                case 4:
                    r = cc255;
                    g = aa255;
                    b = v255;
                    break;
                case 5:
                    r = v255;
                    g = aa255;
                    b = bb255;
                    break;
                }

                pixels[i] = (uint)(a << 24 | r << 16 | g << 8 | b);
            }
        }

        public Color GetPixel(int x, int y)
        {
            return Color.FromArgb((int)this.Pixels[x + y * this.Width]);
        }

        public void SetPixel(int x, int y, Color color)
        {
            this.Pixels[x + y * this.Width] = (uint)color.ToArgb();
        }

        public void Clear()
        {
            uint[] pixels = this.Pixels;
            for (int i = 0; i < pixels.Length; i++)
                pixels[i] = 0;
        }

        public void Fill(Color color)
        {
            uint p = (uint)color.ToArgb();
            uint[] pixels = this.Pixels;
            for (int i = 0; i < pixels.Length; i++)
                pixels[i] = p;
        }

        public void FillRectangle(Rectangle rectangle, Color color)
        {
            uint p = (uint)color.ToArgb();
            uint[] pixels = this.Pixels;

            int i1 = Math.Min(this.Width, Math.Max(0, rectangle.Left));
            int i2 = Math.Min(this.Width, Math.Max(0, rectangle.Right));
            int j1 = Math.Min(this.Height, Math.Max(0, rectangle.Top));
            int j2 = Math.Min(this.Height, Math.Max(0, rectangle.Bottom));
            int width = this.Width;

            for (int j = j1; j < j2; j++)
                for (int i = i1; i < i2; i++)
                    pixels[i + j * width] = p;
        }

        public void RenderText(string text, Point location, Font font, Color color)
        {
            StringFormat gt = StringFormat.GenericTypographic;

            if (text == string.Empty)
                return;

            text = text.Replace('\r', ' ').Replace('\n', ' ')
                   .Replace('\t', ' ').Replace(' ', '\u00a0');

            Size size;
            using (Bitmap b = new Bitmap(1, 1))
            using (Graphics g = Graphics.FromImage(b))
                size = Size.Ceiling(g.MeasureString(text, font, int.MaxValue, gt));
            if (size == Size.Empty)
                return;

            Rectangle rectangle = Rectangle.Intersect(
                new Rectangle(Point.Empty, this.Size), new Rectangle(location, size));
            if (rectangle == Rectangle.Empty)
                return;

            using (Bitmap bitmap =
                new Bitmap(size.Width, size.Height, PixelFormat.Format32bppArgb))
            using (Graphics g = Graphics.FromImage(bitmap))
            {
                g.TextRenderingHint = TextRenderingHint.SingleBitPerPixelGridFit;
                g.DrawString(text, font, Brushes.White, new Point(0, 0), gt);

                uint c = (uint)color.ToArgb();

                BitmapData bitmapData = bitmap.LockBits(
                    new Rectangle(Point.Empty, bitmap.Size),
                    ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb);
                unsafe
                {
                    uint[] pixels = this.Pixels;
                    uint* p = (uint*)bitmapData.Scan0;
                    int x1 = rectangle.Left;
                    int x2 = rectangle.Right;
                    int y1 = rectangle.Top;
                    int y2 = rectangle.Bottom;
                    int width = this.Width;
                    int paddingLeft = Math.Max(0, -location.X);
                    int paddingRight = Math.Max(0, location.X + size.Width - width);

                    if (location.Y < 0)
                        p += -location.Y * size.Width;
                    for (int j = y1; j < y2; j++)
                    {
                        p += paddingLeft;
                        for (int i = x1; i < x2; i++)
                        {
                            if (*p == 0xffffffff)
                                pixels[j * width + i] = c;
                            p++;
                        }
                        p += paddingRight;
                    }
                }
                bitmap.UnlockBits(bitmapData);
            }
        }

        public void RenderTexture(ITexture itexture,
            Rectangle srcRect,
            AffineMatrix mat,
            byte alpha,
            BlendType blendType,
            Tone tone)
        {
            Texture texture = itexture as Texture;
            bool renderSelf = false;
            if (renderSelf = (texture == this))
                texture = this.Clone();

            Point location = new Point((int)mat.TX, (int)mat.TY);

            srcRect.Intersect(new Rectangle(Point.Empty, texture.Size));
            if (srcRect.Width <= 0 || srcRect.Height <= 0)
                return;

            if (mat.A == 1 && mat.B == 0 && mat.C == 0 && mat.D == 1 &&
                blendType == BlendType.Alpha && tone == Tone.Empty)
            {
                this.RenderTexture2(texture, srcRect, location, alpha);
            }
            else
            {
                if (!mat.IsRegular)
                    return;

                PointF pA = mat.Transform(new Point(0, 0));
                PointF pB = mat.Transform(new Point(srcRect.Width, 0));
                PointF pC = mat.Transform(new Point(0, srcRect.Height));
                PointF pD = mat.Transform(new Point(srcRect.Width, srcRect.Height));
                PointF dstPointA = new PointF(
                    Math.Min(pA.X, Math.Min(pB.X, Math.Min(pC.X, pD.X))),
                    Math.Min(pA.Y, Math.Min(pB.Y, Math.Min(pC.Y, pD.Y))));
                PointF dstPointD = new PointF(
                    Math.Max(pA.X, Math.Max(pB.X, Math.Max(pC.X, pD.X))),
                    Math.Max(pA.Y, Math.Max(pB.Y, Math.Max(pC.Y, pD.Y))));

                if (this.Width <= dstPointA.X || this.Height <= dstPointA.Y ||
                    dstPointD.X < 0 || dstPointD.Y < 0)
                    return;

                AffineMatrix matInv = mat;
                matInv.Invert();

                PointF srcO = matInv.Transform(PointF.Add(dstPointA, new SizeF(0.5f, 0.5f)));
                srcO.X += srcRect.X;
                srcO.Y += srcRect.Y;
                PointF srcDX = new PointF((float)matInv.A, (float)matInv.C);
                PointF srcDY = new PointF((float)matInv.B, (float)matInv.D);

                unsafe
                {
                    int x1 = (int)dstPointA.X;
                    int x2 = (int)dstPointD.X;
                    int y1 = (int)dstPointA.Y;
                    int y2 = (int)dstPointD.Y;

                    double srcI;
                    double srcJ;
                    float srcOX = srcO.X;
                    float srcOY = srcO.Y;
                    float srcDXX = srcDX.X;
                    float srcDXY = srcDX.Y;
                    float srcDYX = srcDY.X;
                    float srcDYY = srcDY.Y;
                    int srcWidth = texture.Width;
                    int dstWidth = this.Width;
                    int dstHeight = this.Height;
                    int srcRectLeft = srcRect.Left;
                    int srcRectRight = srcRect.Right;
                    int srcRectTop = srcRect.Top;
                    int srcRectBottom = srcRect.Bottom;
                    uint[] srcP = texture.Pixels;
                    uint[] dstP = this.Pixels;
                    int toneR = tone.Red;
                    int toneG = tone.Green;
                    int toneB = tone.Blue;
                    byte toneS = tone.Saturation;
                    int startI = Math.Max(0, x1);
                    int startJ = Math.Max(0, y1);
                    int endI = Math.Min(this.Width, x2);
                    int endJ = Math.Min(this.Height, y2);

                    for (int j = startJ; j < endJ; j++)
                    {
                        srcI = srcOX + (j - y1) * srcDYX;
                        srcJ = srcOY + (j - y1) * srcDYY;
                        for (int i = startI; i < endI; i++, srcI += srcDXX, srcJ += srcDXY)
                        {
                            int srcIInt = (int)Math.Floor(srcI);
                            int srcJInt = (int)Math.Floor(srcJ);

                            if (srcIInt < srcRectLeft || srcRectRight <= srcIInt ||
                                srcJInt < srcRectTop || srcRectBottom <= srcJInt)
                                continue;

                            uint srcPixel = srcP[srcIInt + srcJInt * srcWidth];
                            int dstI = i + j * dstWidth;
                            uint dstPixel = dstP[dstI];

                            byte srcA = (byte)(srcPixel >> 24);
                            byte srcR = (byte)(srcPixel >> 16);
                            byte srcG = (byte)(srcPixel >> 8);
                            byte srcB = (byte)srcPixel;
                            byte dstA = (byte)(dstPixel >> 24);
                            byte dstR = (byte)(dstPixel >> 16);
                            byte dstG = (byte)(dstPixel >> 8);
                            byte dstB = (byte)dstPixel;

                            srcA = (byte)((srcA * alpha + 255) >> 8);
                            dstA = Math.Max(dstA, srcA);
                            if (toneS < 255)
                            {
                                byte y = (byte)(0.30 * srcR + 0.59 * srcG + 0.11 * srcB);
                                srcR = (byte)(((y << 8) - y + (srcR - y) * toneS + 255) >> 8);
                                srcG = (byte)(((y << 8) - y + (srcG - y) * toneS + 255) >> 8);
                                srcB = (byte)(((y << 8) - y + (srcB - y) * toneS + 255) >> 8);
                            }
                            if (0 < toneR)
                                srcR = (byte)(((srcR << 8) - srcR + (255 - srcR) * toneR + 255) >> 8);
                            else if (toneR < 0)
                                srcR = (byte)(((srcR << 8) - srcR + srcR * toneR + 255) >> 8);
                            if (0 < toneG)
                                srcG = (byte)(((srcG << 8) - srcG + (255 - srcG) * toneG + 255) >> 8);
                            else if (toneG < 0)
                                srcG = (byte)(((srcG << 8) - srcG + srcG * toneG + 255) >> 8);
                            if (0 < toneB)
                                srcB = (byte)(((srcB << 8) - srcB + (255 - srcB) * toneB + 255) >> 8);
                            else if (toneB < 0)
                                srcB = (byte)(((srcB << 8) - srcB + srcB * toneB + 255) >> 8);

                            switch (blendType)
                            {
                            case BlendType.Alpha:
                                dstR = (byte)(((dstR << 8) - dstR + (srcR - dstR) * srcA + 255) >> 8);
                                dstG = (byte)(((dstG << 8) - dstG + (srcG - dstG) * srcA + 255) >> 8);
                                dstB = (byte)(((dstB << 8) - dstB + (srcB - dstB) * srcA + 255) >> 8);
                                break;
                            case BlendType.Addition:
                                dstR = (byte)Math.Min(255, (dstR + ((srcR * srcA + 255) >> 8)));
                                dstG = (byte)Math.Min(255, (dstG + ((srcG * srcA + 255) >> 8)));
                                dstB = (byte)Math.Min(255, (dstB + ((srcB * srcA + 255) >> 8)));
                                break;
                            case BlendType.Subtraction:
                                dstR = (byte)Math.Max(0, (dstR - ((srcR * srcA + 255) >> 8)));
                                dstG = (byte)Math.Max(0, (dstG - ((srcG * srcA + 255) >> 8)));
                                dstB = (byte)Math.Max(0, (dstB - ((srcB * srcA + 255) >> 8)));
                                break;
                            }

                            dstP[dstI] = unchecked((uint)(dstA << 24 | dstR << 16 | dstG << 8 | dstB));
                        }
                    }
                }
            }

            if (renderSelf)
                texture.Dispose();
        }

        private void RenderTexture2(Texture texture, Rectangle srcRect, Point location, byte alpha)
        {
            Debug.Assert(0 <= srcRect.X);
            Debug.Assert(0 <= srcRect.Y);
            Debug.Assert(srcRect.Width <= texture.Width);
            Debug.Assert(srcRect.Height <= texture.Height);

            Rectangle dstRect = Rectangle.Intersect(
                new Rectangle(Point.Empty, this.Size),
                new Rectangle(location, srcRect.Size));
            if (dstRect == Rectangle.Empty)
                return;
            srcRect.Size = dstRect.Size;
            if (location.X < 0)
                srcRect.X += -location.X;
            if (location.Y < 0)
                srcRect.Y += -location.Y;

            unsafe
            {
                uint[] srcP = texture.Pixels;
                uint[] dstP = this.Pixels;

                int width = dstRect.Width;
                int height = dstRect.Height;
                int srcPaddingLeft = srcRect.Left;
                int dstPaddingLeft = dstRect.Left;
                int srcPaddingRight = texture.Width - srcRect.Right;
                int dstPaddingRight = this.Width - dstRect.Right;

                int srcI = srcRect.Y * texture.Width;
                int dstI = dstRect.Y * this.Width;
                for (int j = 0; j < height; j++)
                {
                    srcI += srcPaddingLeft;
                    dstI += dstPaddingLeft;
                    for (int i = 0; i < width; i++)
                    {
                        uint srcPixel = srcP[srcI];
                        uint dstPixel = dstP[dstI];

                        byte srcA = (byte)(srcPixel >> 24);
                        byte srcR = (byte)(srcPixel >> 16);
                        byte srcG = (byte)(srcPixel >> 8);
                        byte srcB = (byte)srcPixel;
                        byte dstA = (byte)(dstPixel >> 24);
                        byte dstR = (byte)(dstPixel >> 16);
                        byte dstG = (byte)(dstPixel >> 8);
                        byte dstB = (byte)dstPixel;

                        srcA = (byte)((srcA * alpha + 255) >> 8);
                        dstA = Math.Max(dstA, srcA);
                        dstR = (byte)(((dstR << 8) - dstR + (srcR - dstR) * srcA + 255) >> 8);
                        dstG = (byte)(((dstG << 8) - dstG + (srcG - dstG) * srcA + 255) >> 8);
                        dstB = (byte)(((dstB << 8) - dstB + (srcB - dstB) * srcA + 255) >> 8);
                        dstP[dstI] = unchecked((uint)(dstA << 24 | dstR << 16 | dstG << 8 | dstB));

                        srcI++;
                        dstI++;
                    }
                    srcI += srcPaddingRight;
                    dstI += dstPaddingRight;
                }
            }
        }
    }
}
