using System;
using System.Drawing.Imaging;
using System.IO;
using Imaging.DDSReader.Utils;

namespace Imaging.DDSReader
{
	public class DDSImage : IDisposable
	{
		private System.Drawing.Bitmap _bitmap;
		private bool _isValid;
		private bool _alpha;

		public System.Drawing.Bitmap BitmapImage
		{
			get { return _bitmap; }
		}

		public bool IsValid
		{
			get { return _isValid; }
		}

		public bool PreserveAlpha
		{
			get { return _alpha; }
			set { _alpha = value; }
		}

		public DDSImage(byte[] ddsImage, bool preserveAlpha = true)
		{
			if (ddsImage == null)
				return;

			if (ddsImage.Length == 0)
				return;

			_alpha = preserveAlpha;

			using (MemoryStream stream = new MemoryStream(ddsImage.Length))
			{
				stream.Write(ddsImage, 0, ddsImage.Length);
				stream.Seek(0, SeekOrigin.Begin);

				using (BinaryReader reader = new BinaryReader(stream))
				{
					Parse(reader);
				}
			}
		}

		public DDSImage(Stream ddsImage, bool preserveAlpha = true)
		{
			if (ddsImage == null)
				return;

			if (!ddsImage.CanRead)
				return;

			_alpha = preserveAlpha;

			using (BinaryReader reader = new BinaryReader(ddsImage))
			{
				Parse(reader);
			}
		}

		public void Dispose()
		{
			if (_bitmap != null)
			{
				_bitmap.Dispose();
				_bitmap = null;
			}
		}

		private void Parse(BinaryReader reader)
		{
			DDSStruct header = new DDSStruct();
			Utils.PixelFormat pixelFormat = Utils.PixelFormat.UNKNOWN;
			byte[] data = null;

			if (ReadHeader(reader, ref header))
			{
				_isValid = true;
				if (header.depth == 0) header.depth = 1;

				uint blocksize = 0;
				pixelFormat = GetFormat(header, ref blocksize);
				if (pixelFormat == Utils.PixelFormat.UNKNOWN)
				{
					throw new InvalidFileHeaderException();
				}

				data = ReadData(reader, header);
				if (data != null)
				{
					byte[] rawData = Decompressor.Expand(header, data, pixelFormat);
					_bitmap = CreateBitmap((int)header.width, (int)header.height, rawData);
				}
			}
		}

		private byte[] ReadData(BinaryReader reader, DDSStruct header)
		{
			byte[] compdata = null;
			uint compsize = 0;

			if ((header.flags & Helper.DDSD_LINEARSIZE) > 1)
			{
				compdata = reader.ReadBytes((int)header.sizeorpitch);
				compsize = (uint)compdata.Length;
			}
			else
			{
				uint bps = header.width * header.pixelformat.rgbbitcount / 8;
				compsize = bps * header.height * header.depth;
				compdata = new byte[compsize];

				MemoryStream mem = new MemoryStream((int)compsize);

				byte[] temp;
				for (int z = 0; z < header.depth; z++)
				{
					for (int y = 0; y < header.height; y++)
					{
						temp = reader.ReadBytes((int)bps);
						mem.Write(temp, 0, temp.Length);
					}
				}
				mem.Seek(0, SeekOrigin.Begin);

				mem.Read(compdata, 0, compdata.Length);
				mem.Close();
			}

			return compdata;
		}

		private System.Drawing.Bitmap CreateBitmap(int width, int height, byte[] rawData)
		{
			var pxFormat = System.Drawing.Imaging.PixelFormat.Format32bppRgb;
			if (_alpha)
				pxFormat = System.Drawing.Imaging.PixelFormat.Format32bppArgb;

			System.Drawing.Bitmap bitmap = new System.Drawing.Bitmap(width, height, pxFormat);

			BitmapData data = bitmap.LockBits(new System.Drawing.Rectangle(0, 0, bitmap.Width, bitmap.Height)
				, ImageLockMode.WriteOnly, pxFormat);
			IntPtr scan = data.Scan0;
			int size = bitmap.Width * bitmap.Height * 4;

			unsafe
			{
				byte* p = (byte*)scan;
				for (int i = 0; i < size; i += 4)
				{
					p[i] = rawData[i + 2]; 
					p[i + 1] = rawData[i + 1];
					p[i + 2] = rawData[i];   
					p[i + 3] = rawData[i + 3]; 
				}
			}

			bitmap.UnlockBits(data);
			return bitmap;
		}

		private bool ReadHeader(BinaryReader reader, ref DDSStruct header)
		{
			byte[] signature = reader.ReadBytes(4);
			if (!(signature[0] == 'D' && signature[1] == 'D' && signature[2] == 'S' && signature[3] == ' '))
				return false;

			header.size = reader.ReadUInt32();
			if (header.size != 124)
				return false;

			header.flags = reader.ReadUInt32();
			header.height = reader.ReadUInt32();
			header.width = reader.ReadUInt32();
			header.sizeorpitch = reader.ReadUInt32();
			header.depth = reader.ReadUInt32();
			header.mipmapcount = reader.ReadUInt32();
			header.alphabitdepth = reader.ReadUInt32();

			header.reserved = new uint[10];
			for (int i = 0; i < 10; i++)
			{
				header.reserved[i] = reader.ReadUInt32();
			}

			header.pixelformat.size = reader.ReadUInt32();
			header.pixelformat.flags = reader.ReadUInt32();
			header.pixelformat.fourcc = reader.ReadUInt32();
			header.pixelformat.rgbbitcount = reader.ReadUInt32();
			header.pixelformat.rbitmask = reader.ReadUInt32();
			header.pixelformat.gbitmask = reader.ReadUInt32();
			header.pixelformat.bbitmask = reader.ReadUInt32();
			header.pixelformat.alphabitmask = reader.ReadUInt32();

			header.ddscaps.caps1 = reader.ReadUInt32();
			header.ddscaps.caps2 = reader.ReadUInt32();
			header.ddscaps.caps3 = reader.ReadUInt32();
			header.ddscaps.caps4 = reader.ReadUInt32();
			header.texturestage = reader.ReadUInt32();

			return true;
		}

		private Utils.PixelFormat GetFormat(DDSStruct header, ref uint blocksize)
		{
			Utils.PixelFormat format = Utils.PixelFormat.UNKNOWN;
			if ((header.pixelformat.flags & Helper.DDPF_FOURCC) == Helper.DDPF_FOURCC)
			{
				blocksize = ((header.width + 3) / 4) * ((header.height + 3) / 4) * header.depth;

				switch (header.pixelformat.fourcc)
				{
					case Helper.FOURCC_DXT1:
						format = Utils.PixelFormat.DXT1;
						blocksize *= 8;
						break;

					case Helper.FOURCC_DXT2:
						format = Utils.PixelFormat.DXT2;
						blocksize *= 16;
						break;

					case Helper.FOURCC_DXT3:
						format = Utils.PixelFormat.DXT3;
						blocksize *= 16;
						break;

					case Helper.FOURCC_DXT4:
						format = Utils.PixelFormat.DXT4;
						blocksize *= 16;
						break;

					case Helper.FOURCC_DXT5:
						format = Utils.PixelFormat.DXT5;
						blocksize *= 16;
						break;

					case Helper.FOURCC_ATI1:
						format = Utils.PixelFormat.ATI1N;
						blocksize *= 8;
						break;

					case Helper.FOURCC_ATI2:
						format = Utils.PixelFormat.THREEDC;
						blocksize *= 16;
						break;

					case Helper.FOURCC_RXGB:
						format = Utils.PixelFormat.RXGB;
						blocksize *= 16;
						break;

					case Helper.FOURCC_DOLLARNULL:
						format = Utils.PixelFormat.A16B16G16R16;
						blocksize = header.width * header.height * header.depth * 8;
						break;

					case Helper.FOURCC_oNULL:
						format = Utils.PixelFormat.R16F;
						blocksize = header.width * header.height * header.depth * 2;
						break;

					case Helper.FOURCC_pNULL:
						format = Utils.PixelFormat.G16R16F;
						blocksize = header.width * header.height * header.depth * 4;
						break;

					case Helper.FOURCC_qNULL:
						format = Utils.PixelFormat.A16B16G16R16F;
						blocksize = header.width * header.height * header.depth * 8;
						break;

					case Helper.FOURCC_rNULL:
						format = Utils.PixelFormat.R32F;
						blocksize = header.width * header.height * header.depth * 4;
						break;

					case Helper.FOURCC_sNULL:
						format = Utils.PixelFormat.G32R32F;
						blocksize = header.width * header.height * header.depth * 8;
						break;

					case Helper.FOURCC_tNULL:
						format = Utils.PixelFormat.A32B32G32R32F;
						blocksize = header.width * header.height * header.depth * 16;
						break;

					default:
						format = Utils.PixelFormat.UNKNOWN;
						blocksize *= 16;
						break;
				}
			}
			else
			{
				if ((header.pixelformat.flags & Helper.DDPF_LUMINANCE) == Helper.DDPF_LUMINANCE)
				{
					if ((header.pixelformat.flags & Helper.DDPF_ALPHAPIXELS) == Helper.DDPF_ALPHAPIXELS)
					{
						format = Utils.PixelFormat.LUMINANCE_ALPHA;
					}
					else
					{
						format = Utils.PixelFormat.LUMINANCE;
					}
				}
				else
				{
					if ((header.pixelformat.flags & Helper.DDPF_ALPHAPIXELS) == Helper.DDPF_ALPHAPIXELS)
					{
						format = Utils.PixelFormat.RGBA;
					}
					else
					{
						format = Utils.PixelFormat.RGB;
					}
				}

				blocksize = (header.width * header.height * header.depth * (header.pixelformat.rgbbitcount >> 3));
			}

			return format;
		}
	}
}