context: //
  //  BLP.cs
  //
  //  Author:
  //       Jarl Gullberg <jarl.gullberg@gmail.com>
  //
  //  Copyright (c) 2017 Jarl Gullberg
  //
  //  This program is free software: you can redistribute it and/or modify
  //  it under the terms of the GNU General Public License as published by
  //  the Free Software Foundation, either version 3 of the License, or
  //  (at your option) any later version.
  //
  //  This program is distributed in the hope that it will be useful,
  //  but WITHOUT ANY WARRANTY; without even the implied warranty of
  //  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  //  GNU General Public License for more details.
  //
  //  You should have received a copy of the GNU General Public License
  //  along with this program.  If not, see <http://www.gnu.org/licenses/>.
  //
  
  using System;
  using System.Collections.Generic;
  using System.IO;
  
  using SixLabors.ImageSharp;
  using SixLabors.ImageSharp.Advanced;
  using SixLabors.ImageSharp.PixelFormats;
  using SixLabors.ImageSharp.Processing;
  using SixLabors.ImageSharp.Processing.Processors.Transforms;
  using SixLabors.Primitives;
  
  using Warcraft.Core.Compression.Squish;
  using Warcraft.Core.Extensions;
  using Warcraft.Core.Structures;
  
  namespace Warcraft.BLP
  {
      /// <summary>
      /// This class represents a BLP binary image and its contained data.
      /// </summary>
      public class BLP
      {
          /// <summary>
          /// Gets or sets the header. This header contains data about the mipmaps stored in the BLP,
          /// and storage information such as offsets and sizes.
          /// </summary>
          public BLPHeader Header { get; set; }
  
          /// <summary>
          /// The palette of colours used in the BLP image. This is not used for DXTC-compressed
          /// textures.
          /// </summary>
          private readonly List<Rgba32> _palette = new List<Rgba32>();
  
          /// <summary>
          /// The size of the JPEG header. This is not used for palettized or DXTC-compressed
          /// textures.
          /// </summary>
          private readonly uint _jpegHeaderSize;
  
          /// <summary>
          /// The JPEG header. This is not used for palettized or DXTC-compressed
          /// textures.
          /// </summary>
          private readonly byte[]? _jpegHeader;
  
          /// <summary>
          /// A list of byte arrays containing the compressed mipmaps.
          /// </summary>
          private readonly List<byte[]> _rawMipMaps = new List<byte[]>();
  
          /// <summary>
          /// Initializes a new instance of the <see cref="Warcraft.BLP.BLP"/> class.
          /// </summary>
          /// <param name="inData">ExtendedData.</param>
          public BLP(byte[] inData)
          {
              using var ms = new MemoryStream(inData);
              using var br = new BinaryReader(ms);
              byte[] fileHeaderBytes;
              if (PeekFormat(br) == BLPFormat.BLP2)
              {
                  fileHeaderBytes = br.ReadBytes(148);
              }
              else
              {
                  fileHeaderBytes = br.ReadBytes(156);
              }
  
              Header = new BLPHeader(fileHeaderBytes);
  
              if (Header.CompressionType == TextureCompressionType.JPEG)
              {
                  _jpegHeaderSize = br.ReadUInt32();
                  _jpegHeader = br.ReadBytes((int)_jpegHeaderSize);
              }
              else if (Header.CompressionType == TextureCompressionType.Palettized)
              {
                  for (var i = 0; i < 256; ++i)
                  {
                      var b = br.ReadByte();
                      var g = br.ReadByte();
                      var r = br.ReadByte();
  
                      // The alpha in the palette is not used, but is stored for the sake of completion.
                      var a = br.ReadByte();
  
                      var paletteColor = new Rgba32(r, g, b, a);
                      _palette.Add(paletteColor);
                  }
              }
              else
              {
                  // Fill up an empty palette - the palette is always present, but we'll be going after offsets anyway
                  for (var i = 0; i < 256; ++i)
                  {
                      var paletteColor = default(Rgba32);
                      _palette.Add(paletteColor);
                  }
              }
  
              // Read the raw mipmap data
              for (var i = 0; i < Header.GetNumMipMaps(); ++i)
              {
                  br.BaseStream.Position = Header.MipMapOffsets[i];
                  _rawMipMaps.Add(br.ReadBytes((int)Header.MipMapSizes[i]));
              }
          }
  
          /// <summary>
          /// Peeks the format of the current file.
          /// </summary>
          /// <returns>The format.</returns>
          /// <param name="br">Br.</param>
          /// <exception cref="FileLoadException">If no format was detected, a FileLoadException will be thrown.</exception>
          private static BLPFormat PeekFormat(BinaryReader br)
          {
              var startPosition = br.BaseStream.Position;
              var dataSignature = new string(br.ReadChars(4));
  
              BLPFormat format;
              if (!Enum.TryParse(dataSignature, out format))
              {
                  throw new FileLoadException("The provided data did not have a BLP signature.");
              }
  
              br.BaseStream.Position = startPosition;
              return format;
          }
  
          /// <summary>
          /// Initializes a new instance of the <see cref="Warcraft.BLP.BLP"/> class.
          /// This constructor creates a BLP file using the specified compression from a bitmap object.
          /// If the compression type specifed is DXTC, the default pixel format used is DXT1 for opaque textures and DXT3 for the rest.
          /// </summary>
          /// <param name="image">Image.</param>
          /// <param name="compressionType">Compression type.</param>
          public BLP(Image<Rgba32> image, TextureCompressionType compressionType)
          {
              // Set up the header
              Header = new BLPHeader
              {
                  CompressionType = compressionType
              };
  
              if (compressionType == TextureCompressionType.Palettized)
              {
                  Header.PixelFormat = BLPPixelFormat.Palettized;
  
                  // Determine best alpha bit depth
                  if (image.HasAlpha())
                  {
                      var alphaLevels = new List<byte>();
                      for (var y = 0; y < image.Height; ++y)
                      {
                          for (var x = 0; x < image.Width; ++x)
                          {
                              var pixel = image[x, y];
                              if (!alphaLevels.Contains(pixel.A))
                              {
                                  alphaLevels.Add(pixel.A);
                              }
  
                              if (alphaLevels.Count > 16)
                              {
                                  break;
                              }
                          }
                      }
  
                      if (alphaLevels.Count > 16)
                      {
                          // More than 16? Use a full byte
                          Header.AlphaBitDepth = 8;
                      }
                      else if (alphaLevels.Count > 2)
                      {
                          // More than 2, but less than or equal to 16? Use half a byte
                          Header.AlphaBitDepth = 4;
                      }
                      else
                      {
                          // Just 2? Use a bit instead
                          Header.AlphaBitDepth = 1;
                      }
                  }
                  else
                  {
                      // No alpha, so a bit depth of 0.
                      Header.AlphaBitDepth = 0;
                  }
              }
              else if (compressionType == TextureCompressionType.DXTC)
              {
                  Header.AlphaBitDepth = 8;
  
                  // Determine best DXTC type (1, 3 or 5)
                  if (image.HasAlpha())
                  {
                      Header.PixelFormat = BLPPixelFormat.DXT3;
                  }
                  else
                  {
                      // DXT1 for no alpha
                      Header.PixelFormat = BLPPixelFormat.DXT1;
                  }
              }
              else if (compressionType == TextureCompressionType.Uncompressed)
              {
                  // The alpha will be stored as a straight ARGB texture, so set it to 8
                  Header.AlphaBitDepth = 8;
                  Header.PixelFormat = BLPPixelFormat.PalARGB1555DitherFloydSteinberg;
              }
  
              // What the mip type does is currently unknown, but it's usually set to 1.
              Header.MipMapType = 1;
              Header.Resolution = new Resolution((uint)image.Width, (uint)image.Height);
  
              // It's now time to compress the image
              _rawMipMaps = CompressImage(image);
  
              // Calculate the offsets and sizes
              var mipOffset = (uint)(Header.GetSize() + (_palette.Count * 4));
              foreach (var rawMipMap in _rawMipMaps)
              {
                  var mipSize = (uint)rawMipMap.Length;
  
                  Header.MipMapOffsets.Add(mipOffset);
                  Header.MipMapSizes.Add(mipSize);
  
                  // Push the offset ahead for the next mipmap
                  mipOffset += mipSize;
              }
          }
  
          /// <summary>
          /// Gets the format of the BLP image.
          /// </summary>
          /// <returns>The format.</returns>
          public BLPFormat GetFormat()
          {
              return Header.Format;
          }
  
          /// <summary>
          /// Gets a list of formatted strings describing the mipmap levels.
          /// </summary>
          /// <returns>The mip map level strings.</returns>
          public IEnumerable<string> GetMipMapLevelStrings()
          {
              for (uint i = 0; i < GetMipMapCount(); ++i)
              {
                  yield return $"{i}: {GetMipLevelResolution(i)}";
              }
          }
  
          /// <summary>
          /// Gets the resolution of the specified mip level.
          /// </summary>
          /// <returns>The mip level resolution.</returns>
          /// <param name="mipLevel">Mip level.</param>
          public Resolution GetMipLevelResolution(uint mipLevel)
          {
              var targetXRes = GetLevelAdjustedResolutionValue(GetResolution().X, mipLevel);
              var targetYRes = GetLevelAdjustedResolutionValue(GetResolution().Y, mipLevel);
  
              return new Resolution(targetXRes, targetYRes);
          }
  
          /// <summary>
          /// Gets a bitmap representing the given zero-based mipmap level. This creates a new <see cref="Image{Rgba32}"/> object
          /// from one of the raw mipmaps stored in the image.
          /// </summary>
          /// <returns>A bitmap.</returns>
          /// <param name="mipLevel">Mipmap level.</param>
          public Image<Rgba32> GetMipMap(uint mipLevel)
          {
              return DecompressMipMap(_rawMipMaps[(int)mipLevel], mipLevel);
          }
  
          /// <summary>
          /// Gets the compressed data for the specified mipmap level.
          /// </summary>
          /// <exception cref="ArgumentOutOfRangeException">
          /// Throws if the input level is greated than the maximum stored level.
          /// </exception>
          /// <returns>A byte array containing the compressed data.</returns>
          /// <param name="mipLevel">The zero-based mipmap level.</param>
          public byte[] GetRawMipMap(uint mipLevel)
          {
              if (mipLevel > _rawMipMaps.Count - 1)
              {
                  throw new ArgumentOutOfRangeException
                  (
                      nameof(mipLevel),
                      mipLevel,
                      "The requested mip level was greater than the maximum stored level."
                  );
              }
  
              return _rawMipMaps[(int)mipLevel];
          }
  
          /// <summary>
          /// Gets a resolution value (height or width) adjusted by the specified mipmap level. In practice, this is
          /// a division by the level as far as possible.
          ///
          /// A greater mip returns a lower resolution value, to a maximum of 1.
          /// </summary>
          /// <param name="resolutionValue">The value to adjust. This must be a power-of-two value.</param>
          /// <param name="mipLevel">The mipmap level to adjust by.</param>
          /// <returns>The adjusted value.</returns>
          private static uint GetLevelAdjustedResolutionValue(uint resolutionValue, uint mipLevel)
          {
              if ((resolutionValue != 0) && (resolutionValue & (resolutionValue - 1)) != 0)
              {
                  throw new ArgumentException
                  (
                      "The input resolution value must be a power-of-two value.",
                      nameof(resolutionValue)
                  );
              }
  
              return resolutionValue / (uint)Math.Pow(2, mipLevel).Clamp(1, resolutionValue);
          }
  
          /// <summary>
          /// Decompresses a mipmap in the file at the specified level from the specified data.
          /// </summary>
          /// <returns>The mipmap.</returns>
          /// <param name="inData">ExtendedData containing the mipmap level.</param>
          /// <param name="mipLevel">The mipmap level of the data.</param>
          private Image<Rgba32> DecompressMipMap(byte[] inData, uint mipLevel)
          {
              if (inData == null || inData.Length <= 0)
              {
                  throw new ArgumentException("No image data provided.", nameof(inData));
              }
  
              Image<Rgba32>? map = null;
              var targetXRes = GetLevelAdjustedResolutionValue(GetResolution().X, mipLevel);
              var targetYRes = GetLevelAdjustedResolutionValue(GetResolution().Y, mipLevel);
  
              if (targetXRes <= 0 || targetYRes <= 0)
              {
                  throw new ArgumentException
                  (
                      $"The input mipmap level produced an invalid resolution: {mipLevel}",
                      nameof(mipLevel)
                  );
              }
  
              switch (Header.CompressionType)
              {
                  case TextureCompressionType.Palettized:
                  {
                      map = new Image<Rgba32>((int)targetXRes, (int)targetYRes);
                      using var ms = new MemoryStream(inData);
                      using var br = new BinaryReader(ms);
  
                      // Read colour information
                      for (var y = 0; y < targetYRes; ++y)
                      {
  
gt:                         for (var x = 0; x < targetXRes; ++x)