﻿using System;
using System.Collections.Generic;
using System.IO;

namespace Oni.Imaging
{
    internal class DdsHeader
    {
        #region Private data

        private enum FOURCC
        {
            FOURCC_NONE = 0,
            FOURCC_DXT1 = 0x31545844
        }

        [Flags]
        private enum DDS_FLAGS
        {
            DDSD_CAPS = 0x00000001,
            DDSD_HEIGHT = 0x00000002,
            DDSD_WIDTH = 0x00000004,
            DDSD_PITCH = 0x00000008,
            DDSD_PIXELFORMAT = 0x00001000,
            DDSD_MIPMAPCOUNT = 0x00020000,
            DDSD_LINEARSIZE = 0x00080000,
            DDSD_DEPTH = 0x00800000
        }

        [Flags]
        private enum DDP_FLAGS
        {
            DDPF_RGB = 0x00000040,
            DDPF_FOURCC = 0x00000004,
            DDPF_ALPHAPIXELS = 0x00000001
        }

        [Flags]
        private enum DDS_CAPS
        {
            DDSCAPS_TEXTURE = 0x00001000,
            DDSCAPS_MIPMAP = 0x00400000,
            DDSCAPS_COMPLEX = 0x00000008
        }

        [Flags]
        private enum DDS_CAPS2
        {
            DDSCAPS2_CUBEMAP = 0x00000200,
            DDSCAPS2_VOLUME = 0x00200000
        }

        private const int DDS_MAGIC = 0x20534444;

        private DDS_FLAGS flags;
        private int height;
        private int width;
        private int linearSize;
        private int depth;
        private int mipmapCount;
        private DDP_FLAGS formatFlags;
        private FOURCC fourCC;
        private int rgbBitCount;
        private uint rBitMask;
        private uint gBitMask;
        private uint bBitMask;
        private uint aBitMask;
        private DDS_CAPS caps;
        private DDS_CAPS2 caps2;
        #endregion

        public int Width => width;
        public int Height => height;
        public int MipmapCount => mipmapCount;

        public SurfaceFormat GetSurfaceFormat()
        {
            if (fourCC == FOURCC.FOURCC_DXT1)
                return SurfaceFormat.DXT1;

            if (rgbBitCount == 32)
            {
                if (rBitMask == 0x00ff0000 && gBitMask == 0x0000ff00 && bBitMask == 0x000000ff)
                {
                    if ((formatFlags & DDP_FLAGS.DDPF_ALPHAPIXELS) == 0)
                        return SurfaceFormat.BGRX;

                    if (aBitMask == 0xff000000)
                        return SurfaceFormat.BGRA;
                }
            }
            else if (rgbBitCount == 16)
            {
                if (rBitMask == 0x7c00 && gBitMask == 0x03e0 && bBitMask == 0x001f)
                {
                    if ((formatFlags & DDP_FLAGS.DDPF_ALPHAPIXELS) == 0)
                        return SurfaceFormat.BGRX5551;

                    if (aBitMask == 0x8000)
                        return SurfaceFormat.BGRA5551;
                }
                else if (rBitMask == 0x0f00 && gBitMask == 0x00f0 && bBitMask == 0x000f)
                {
                    if ((formatFlags & DDP_FLAGS.DDPF_ALPHAPIXELS) != 0)
                        return SurfaceFormat.BGRA4444;
                }
            }

            throw new NotSupportedException(string.Format("Unsupported pixel format {0} {1} {2} {3} {4} {5} {6}",
                formatFlags,
                fourCC, rgbBitCount,
                rBitMask, gBitMask, bBitMask, aBitMask));
        }

        public static DdsHeader Read(BinaryReader reader)
        {
            if (reader.ReadInt32() != DDS_MAGIC)
                throw new InvalidDataException("Not a DDS file");

            var header = new DdsHeader();

            if (reader.ReadInt32() != 124)
                throw new InvalidDataException("Invalid DDS header size");

            header.flags = (DDS_FLAGS)reader.ReadInt32();

            var requiredFlags = DDS_FLAGS.DDSD_CAPS | DDS_FLAGS.DDSD_HEIGHT | DDS_FLAGS.DDSD_WIDTH | DDS_FLAGS.DDSD_PIXELFORMAT;

            if ((header.flags & requiredFlags) != requiredFlags)
                throw new InvalidDataException(string.Format("Invalid DDS header flags ({0})", header.flags));

            header.height = reader.ReadInt32();
            header.width = reader.ReadInt32();

            if (header.width == 0 || header.height == 0)
                throw new InvalidDataException("DDS file has 0 width or height");

            header.linearSize = reader.ReadInt32();
            header.depth = reader.ReadInt32();

            if ((header.flags & DDS_FLAGS.DDSD_MIPMAPCOUNT) != 0)
            {
                header.mipmapCount = reader.ReadInt32();
            }
            else
            {
                reader.ReadInt32();
                header.mipmapCount = 1;
            }

            reader.Position += 44;

            if (reader.ReadInt32() != 32)
                throw new InvalidDataException("Invalid DDS pixel format size");

            header.formatFlags = (DDP_FLAGS)reader.ReadInt32();

            if ((header.formatFlags & DDP_FLAGS.DDPF_FOURCC) != 0)
            {
                header.fourCC = (FOURCC)reader.ReadInt32();
            }
            else
            {
                reader.ReadInt32();
                header.fourCC = FOURCC.FOURCC_NONE;
            }

            header.rgbBitCount = reader.ReadInt32();
            header.rBitMask = reader.ReadUInt32();
            header.gBitMask = reader.ReadUInt32();
            header.bBitMask = reader.ReadUInt32();
            header.aBitMask = reader.ReadUInt32();

            header.caps = (DDS_CAPS)reader.ReadInt32();
            header.caps2 = (DDS_CAPS2)reader.ReadInt32();

            reader.Position += 12;

            if (header.fourCC == FOURCC.FOURCC_NONE)
            {
                if (header.rgbBitCount != 16 && header.rgbBitCount != 32)
                    throw new NotSupportedException(string.Format("Unsupported RGB bit count {0}", header.rgbBitCount));
            }
            else if (header.fourCC != FOURCC.FOURCC_DXT1)
            {
                throw new NotSupportedException(string.Format("Unsupported FOURCC {0}", header.fourCC));
            }

            return header;
        }

        public static DdsHeader Create(IList<Surface> surfaces)
        {
            var header = new DdsHeader();

            int width = surfaces[0].Width;
            int height = surfaces[0].Height;
            var format = surfaces[0].Format;

            header.flags = DDS_FLAGS.DDSD_CAPS | DDS_FLAGS.DDSD_HEIGHT | DDS_FLAGS.DDSD_WIDTH | DDS_FLAGS.DDSD_PIXELFORMAT;
            header.width = width;
            header.height = height;
            header.caps = DDS_CAPS.DDSCAPS_TEXTURE;

            switch (format)
            {
                case SurfaceFormat.BGRA4444:
                    header.formatFlags = DDP_FLAGS.DDPF_RGB | DDP_FLAGS.DDPF_ALPHAPIXELS;
                    header.rgbBitCount = 16;
                    header.aBitMask = 0xf000;
                    header.rBitMask = 0x0f00;
                    header.gBitMask = 0x00f0;
                    header.bBitMask = 0x000f;
                    break;
                case SurfaceFormat.BGRX5551:
                case SurfaceFormat.BGRA5551:
                    header.formatFlags = DDP_FLAGS.DDPF_RGB | DDP_FLAGS.DDPF_ALPHAPIXELS;
                    header.rgbBitCount = 16;
                    header.aBitMask = 0x8000;
                    header.rBitMask = 0x7c00;
                    header.gBitMask = 0x03e0;
                    header.bBitMask = 0x001f;
                    break;
                case SurfaceFormat.BGRA:
                    header.formatFlags = DDP_FLAGS.DDPF_RGB | DDP_FLAGS.DDPF_ALPHAPIXELS;
                    header.rgbBitCount = 32;
                    header.aBitMask = 0xff000000;
                    header.rBitMask = 0x00ff0000;
                    header.gBitMask = 0x0000ff00;
                    header.bBitMask = 0x000000ff;
                    break;
                case SurfaceFormat.BGRX:
                    header.formatFlags = DDP_FLAGS.DDPF_RGB;
                    header.rgbBitCount = 32;
                    header.rBitMask = 0x00ff0000;
                    header.gBitMask = 0x0000ff00;
                    header.bBitMask = 0x000000ff;
                    break;
                case SurfaceFormat.RGBA:
                    header.formatFlags = DDP_FLAGS.DDPF_RGB | DDP_FLAGS.DDPF_ALPHAPIXELS;
                    header.rgbBitCount = 32;
                    header.aBitMask = 0x000000ff;
                    header.rBitMask = 0x0000ff00;
                    header.gBitMask = 0x00ff0000;
                    header.bBitMask = 0xff000000;
                    break;
                case SurfaceFormat.RGBX:
                    header.formatFlags = DDP_FLAGS.DDPF_RGB;
                    header.rgbBitCount = 32;
                    header.rBitMask = 0x0000ff00;
                    header.gBitMask = 0x00ff0000;
                    header.bBitMask = 0xff000000;
                    break;
                case SurfaceFormat.DXT1:
                    header.formatFlags = DDP_FLAGS.DDPF_FOURCC;
                    header.fourCC = FOURCC.FOURCC_DXT1;
                    break;
            }

            switch (format)
            {
                case SurfaceFormat.BGRA4444:
                case SurfaceFormat.BGRX5551:
                case SurfaceFormat.BGRA5551:
                    header.flags |= DDS_FLAGS.DDSD_PITCH;
                    header.linearSize = width * 2;
                    break;
                case SurfaceFormat.BGRX:
                case SurfaceFormat.BGRA:
                case SurfaceFormat.RGBA:
                case SurfaceFormat.RGBX:
                    header.flags |= DDS_FLAGS.DDSD_PITCH;
                    header.linearSize = width * 4;
                    break;
                case SurfaceFormat.DXT1:
                    header.flags |= DDS_FLAGS.DDSD_LINEARSIZE;
                    header.linearSize = Math.Max(1, width / 4) * Math.Max(1, height / 4) * 8;
                    break;
            }

            if (surfaces.Count > 1)
            {
                header.flags |= DDS_FLAGS.DDSD_MIPMAPCOUNT;
                header.mipmapCount = surfaces.Count;
                header.caps |= DDS_CAPS.DDSCAPS_COMPLEX | DDS_CAPS.DDSCAPS_MIPMAP;
            }

            return header;
        }

        public void Write(BinaryWriter writer)
        {
            writer.Write(DDS_MAGIC);
            writer.Write(124);
            writer.Write((int)flags);
            writer.Write(height);
            writer.Write(width);
            writer.Write(linearSize);
            writer.Write(depth);
            writer.Write(mipmapCount);
            writer.BaseStream.Seek(44, SeekOrigin.Current);
            writer.Write(32);
            writer.Write((int)formatFlags);
            writer.Write((int)fourCC);
            writer.Write(rgbBitCount);
            writer.Write(rBitMask);
            writer.Write(gBitMask);
            writer.Write(bBitMask);
            writer.Write(aBitMask);
            writer.Write((int)caps);
            writer.Write((int)caps2);
            writer.BaseStream.Seek(12, SeekOrigin.Current);
        }
    }
}
