// File: crn_ktx_texture.cpp
#include "crn_core.h"
#include "crn_ktx_texture.h"
#include "crn_console.h"

// Set #if CRNLIB_KTX_PVRTEX_WORKAROUNDS to 1 to enable various workarounds for oddball KTX files written by PVRTexTool.
#define CRNLIB_KTX_PVRTEX_WORKAROUNDS 1

namespace crnlib
{
   const uint8 s_ktx_file_id[12] = { 0xAB, 0x4B, 0x54, 0x58, 0x20, 0x31, 0x31, 0xBB, 0x0D, 0x0A, 0x1A, 0x0A };

   bool is_packed_pixel_ogl_type(uint32 ogl_type)
   {
      switch (ogl_type)
      {
         case KTX_UNSIGNED_BYTE_3_3_2:
         case KTX_UNSIGNED_BYTE_2_3_3_REV:
         case KTX_UNSIGNED_SHORT_5_6_5:
         case KTX_UNSIGNED_SHORT_5_6_5_REV:
         case KTX_UNSIGNED_SHORT_4_4_4_4:
         case KTX_UNSIGNED_SHORT_4_4_4_4_REV:
         case KTX_UNSIGNED_SHORT_5_5_5_1:
         case KTX_UNSIGNED_SHORT_1_5_5_5_REV:
         case KTX_UNSIGNED_INT_8_8_8_8:
         case KTX_UNSIGNED_INT_8_8_8_8_REV:
         case KTX_UNSIGNED_INT_10_10_10_2:
         case KTX_UNSIGNED_INT_2_10_10_10_REV:
         case KTX_UNSIGNED_INT_24_8:
         case KTX_UNSIGNED_INT_10F_11F_11F_REV:
         case KTX_UNSIGNED_INT_5_9_9_9_REV:
            return true;
      }
      return false;
   }

   uint get_ogl_type_size(uint32 ogl_type)
   {
      switch (ogl_type)
      {
         case KTX_UNSIGNED_BYTE:
         case KTX_BYTE:
            return 1;
         case KTX_HALF_FLOAT:
         case KTX_UNSIGNED_SHORT:
         case KTX_SHORT:
            return 2;
         case KTX_FLOAT:
         case KTX_UNSIGNED_INT:
         case KTX_INT:
            return 4;
         case KTX_UNSIGNED_BYTE_3_3_2:
         case KTX_UNSIGNED_BYTE_2_3_3_REV:
            return 1;
         case KTX_UNSIGNED_SHORT_5_6_5:
         case KTX_UNSIGNED_SHORT_5_6_5_REV:
         case KTX_UNSIGNED_SHORT_4_4_4_4:
         case KTX_UNSIGNED_SHORT_4_4_4_4_REV:
         case KTX_UNSIGNED_SHORT_5_5_5_1:
         case KTX_UNSIGNED_SHORT_1_5_5_5_REV:
            return 2;
         case KTX_UNSIGNED_INT_8_8_8_8:
         case KTX_UNSIGNED_INT_8_8_8_8_REV:
         case KTX_UNSIGNED_INT_10_10_10_2:
         case KTX_UNSIGNED_INT_2_10_10_10_REV:
         case KTX_UNSIGNED_INT_24_8:
         case KTX_UNSIGNED_INT_10F_11F_11F_REV:
         case KTX_UNSIGNED_INT_5_9_9_9_REV:
            return 4;
      }
      return 0;
   }

   uint32 get_ogl_base_internal_fmt(uint32 ogl_fmt)
   {
      switch (ogl_fmt)
      {
         case KTX_ETC1_RGB8_OES:
         case KTX_RGB_S3TC:
         case KTX_RGB4_S3TC:
         case KTX_COMPRESSED_RGB_S3TC_DXT1_EXT:
         case KTX_COMPRESSED_SRGB_S3TC_DXT1_EXT:
            return KTX_RGB;
         case KTX_COMPRESSED_RGBA_S3TC_DXT1_EXT:
         case KTX_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT:
         case KTX_RGBA_S3TC:
         case KTX_RGBA4_S3TC:
         case KTX_COMPRESSED_RGBA_S3TC_DXT3_EXT:
         case KTX_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT:
         case KTX_COMPRESSED_RGBA_S3TC_DXT5_EXT:
         case KTX_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT:
         case KTX_RGBA_DXT5_S3TC:
         case KTX_RGBA4_DXT5_S3TC:
            return KTX_RGBA;
         case 1:
         case KTX_RED:
         case KTX_RED_INTEGER:
         case KTX_GREEN:
         case KTX_GREEN_INTEGER:
         case KTX_BLUE:
         case KTX_BLUE_INTEGER:
         case KTX_R8:
         case KTX_R8UI:
         case KTX_LUMINANCE8:
         case KTX_ALPHA:
         case KTX_LUMINANCE:
         case KTX_COMPRESSED_RED_RGTC1_EXT:
         case KTX_COMPRESSED_SIGNED_RED_RGTC1_EXT:
         case KTX_COMPRESSED_LUMINANCE_LATC1_EXT:
         case KTX_COMPRESSED_SIGNED_LUMINANCE_LATC1_EXT:
            return KTX_RED;
         case 2:
         case KTX_RG:
         case KTX_RG8:
         case KTX_RG_INTEGER:
         case KTX_LUMINANCE_ALPHA:
         case KTX_COMPRESSED_RED_GREEN_RGTC2_EXT:
         case KTX_COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT:
         case KTX_COMPRESSED_LUMINANCE_ALPHA_LATC2_EXT:
         case KTX_COMPRESSED_SIGNED_LUMINANCE_ALPHA_LATC2_EXT:
            return KTX_RG;
         case 3:
         case KTX_SRGB:
         case KTX_RGB:
         case KTX_RGB_INTEGER:
         case KTX_BGR:
         case KTX_BGR_INTEGER:
         case KTX_RGB8:
         case KTX_SRGB8:
            return KTX_RGB;
         case 4:
         case KTX_RGBA:
         case KTX_BGRA:
         case KTX_RGBA_INTEGER:
         case KTX_BGRA_INTEGER:
         case KTX_SRGB_ALPHA:
         case KTX_SRGB8_ALPHA8:
         case KTX_RGBA8:
            return KTX_RGBA;
      }
      return 0;
   }

   bool get_ogl_fmt_desc(uint32 ogl_fmt, uint32 ogl_type, uint& block_dim, uint& bytes_per_block)
   {
      uint ogl_type_size = get_ogl_type_size(ogl_type);

      block_dim = 1;
      bytes_per_block = 0;

      switch (ogl_fmt)
      {
         case KTX_COMPRESSED_RED_RGTC1_EXT:
         case KTX_COMPRESSED_SIGNED_RED_RGTC1_EXT:
         case KTX_COMPRESSED_LUMINANCE_LATC1_EXT:
         case KTX_COMPRESSED_SIGNED_LUMINANCE_LATC1_EXT:
         case KTX_ETC1_RGB8_OES:
         case KTX_RGB_S3TC:
         case KTX_RGB4_S3TC:
         case KTX_COMPRESSED_RGB_S3TC_DXT1_EXT:
         case KTX_COMPRESSED_RGBA_S3TC_DXT1_EXT:
         case KTX_COMPRESSED_SRGB_S3TC_DXT1_EXT:
         case KTX_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT:
         {
            block_dim = 4;
            bytes_per_block = 8;
            break;
         }
         case KTX_COMPRESSED_LUMINANCE_ALPHA_LATC2_EXT:
         case KTX_COMPRESSED_SIGNED_LUMINANCE_ALPHA_LATC2_EXT:
         case KTX_COMPRESSED_RED_GREEN_RGTC2_EXT:
         case KTX_COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT:
         case KTX_RGBA_S3TC:
         case KTX_RGBA4_S3TC:
         case KTX_COMPRESSED_RGBA_S3TC_DXT3_EXT:
         case KTX_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT:
         case KTX_COMPRESSED_RGBA_S3TC_DXT5_EXT:
         case KTX_COMPRESSED_SRGB_ALPHA_S3TC_DXT5_EXT:
         case KTX_RGBA_DXT5_S3TC:
         case KTX_RGBA4_DXT5_S3TC:
         {
            block_dim = 4;
            bytes_per_block = 16;
            break;
         }
         case 1:
         case KTX_ALPHA:
         case KTX_RED:
         case KTX_GREEN:
         case KTX_BLUE:
         case KTX_RED_INTEGER:
         case KTX_GREEN_INTEGER:
         case KTX_BLUE_INTEGER:
         case KTX_LUMINANCE:
         {
            bytes_per_block = ogl_type_size;
            break;
         }
         case KTX_R8:
         case KTX_R8UI:
         case KTX_ALPHA8:
         case KTX_LUMINANCE8:
         {
            bytes_per_block = 1;
            break;
         }
         case 2:
         case KTX_RG:
         case KTX_RG_INTEGER:
         case KTX_LUMINANCE_ALPHA:
         {
            bytes_per_block = 2 * ogl_type_size;
            break;
         }
         case KTX_RG8:
         case KTX_LUMINANCE8_ALPHA8:
         {
            bytes_per_block = 2;
            break;
         }
         case 3:
         case KTX_SRGB:
         case KTX_RGB:
         case KTX_BGR:
         case KTX_RGB_INTEGER:
         case KTX_BGR_INTEGER:
         {
            bytes_per_block = is_packed_pixel_ogl_type(ogl_type) ? ogl_type_size : (3 * ogl_type_size);
            break;
         }
         case KTX_RGB8:
         case KTX_SRGB8:
         {
            bytes_per_block = 3;
            break;
         }
         case 4:
         case KTX_RGBA:
         case KTX_BGRA:
         case KTX_RGBA_INTEGER:
         case KTX_BGRA_INTEGER:
         case KTX_SRGB_ALPHA:
         {
            bytes_per_block = is_packed_pixel_ogl_type(ogl_type) ? ogl_type_size : (4 * ogl_type_size);
            break;
         }
         case KTX_SRGB8_ALPHA8:
         case KTX_RGBA8:
         {
            bytes_per_block = 4;
            break;
         }
         default:
            return false;
      }
      return true;
   }

   bool ktx_texture::compute_pixel_info()
   {
      if ((!m_header.m_glType) || (!m_header.m_glFormat))
      {
         if ((m_header.m_glType) || (m_header.m_glFormat))
            return false;

         // Must be a compressed format.
         if (!get_ogl_fmt_desc(m_header.m_glInternalFormat, m_header.m_glType, m_block_dim, m_bytes_per_block))
         {
#if CRNLIB_KTX_PVRTEX_WORKAROUNDS
            if ((!m_header.m_glInternalFormat) && (!m_header.m_glType) && (!m_header.m_glTypeSize) && (!m_header.m_glBaseInternalFormat))
            {
               // PVRTexTool writes bogus headers when outputting ETC1.
               console::warning("ktx_texture::compute_pixel_info: Header doesn't specify any format, assuming ETC1 and hoping for the best");
               m_header.m_glBaseInternalFormat = KTX_RGB;
               m_header.m_glInternalFormat = KTX_ETC1_RGB8_OES;
               m_header.m_glTypeSize = 1;
               m_block_dim = 4;
               m_bytes_per_block = 8;
               return true;
            }
#endif
            return false;
         }

         if (m_block_dim == 1)
            return false;
      }
      else
      {
         // Must be an uncompressed format.
         if (!get_ogl_fmt_desc(m_header.m_glFormat, m_header.m_glType, m_block_dim, m_bytes_per_block))
            return false;

         if (m_block_dim > 1)
            return false;
      }
      return true;
   }

   bool ktx_texture::read_from_stream(data_stream_serializer& serializer)
   {
      clear();

      // Read header
      if (serializer.read(&m_header, 1, sizeof(m_header)) != sizeof(ktx_header))
         return false;

      // Check header
      if (memcmp(s_ktx_file_id, m_header.m_identifier, sizeof(m_header.m_identifier)))
         return false;

      if ((m_header.m_endianness != KTX_OPPOSITE_ENDIAN) && (m_header.m_endianness != KTX_ENDIAN))
         return false;

      m_opposite_endianness = (m_header.m_endianness == KTX_OPPOSITE_ENDIAN);
      if (m_opposite_endianness)
      {
         m_header.endian_swap();

         if ((m_header.m_glTypeSize != sizeof(uint8)) && (m_header.m_glTypeSize != sizeof(uint16)) && (m_header.m_glTypeSize != sizeof(uint32)))
            return false;
      }

      if (!check_header())
         return false;

      if (!compute_pixel_info())
         return false;

      uint8 pad_bytes[3];

      // Read the key value entries
      uint num_key_value_bytes_remaining = m_header.m_bytesOfKeyValueData;
      while (num_key_value_bytes_remaining)
      {
         if (num_key_value_bytes_remaining < sizeof(uint32))
            return false;

         uint32 key_value_byte_size;
         if (serializer.read(&key_value_byte_size, 1, sizeof(uint32)) != sizeof(uint32))
            return false;

         num_key_value_bytes_remaining -= sizeof(uint32);

         if (m_opposite_endianness)
            key_value_byte_size = utils::swap32(key_value_byte_size);

         if (key_value_byte_size > num_key_value_bytes_remaining)
            return false;

         uint8_vec key_value_data;
         if (key_value_byte_size)
         {
            key_value_data.resize(key_value_byte_size);
            if (serializer.read(&key_value_data[0], 1, key_value_byte_size) != key_value_byte_size)
               return false;
         }

         m_key_values.push_back(key_value_data);

         uint padding = 3 - ((key_value_byte_size + 3) % 4);
         if (padding)
         {
            if (serializer.read(pad_bytes, 1, padding) != padding)
               return false;
         }

         num_key_value_bytes_remaining -= key_value_byte_size;
         if (num_key_value_bytes_remaining < padding)
            return false;
         num_key_value_bytes_remaining -= padding;
      }

      // Now read the mip levels
      uint total_faces = get_num_mips() * get_array_size() * get_num_faces() * get_depth();
      if ((!total_faces) || (total_faces > 65535))
         return false;

      // See Section 2.8 of KTX file format: No rounding to block sizes should be applied for block compressed textures.
      // OK, I'm going to break that rule otherwise KTX can only store a subset of textures that DDS can handle for no good reason.
#if 0
      const uint mip0_row_blocks = m_header.m_pixelWidth / m_block_dim;
      const uint mip0_col_blocks = CRNLIB_MAX(1, m_header.m_pixelHeight) / m_block_dim;
#else
      const uint mip0_row_blocks = (m_header.m_pixelWidth + m_block_dim - 1) / m_block_dim;
      const uint mip0_col_blocks = (CRNLIB_MAX(1, m_header.m_pixelHeight) + m_block_dim - 1) / m_block_dim;
#endif
      if ((!mip0_row_blocks) || (!mip0_col_blocks))
         return false;

      const uint mip0_depth = CRNLIB_MAX(1, m_header.m_pixelDepth); mip0_depth;

      bool has_valid_image_size_fields = true;
      bool disable_mip_and_cubemap_padding = false;

#if CRNLIB_KTX_PVRTEX_WORKAROUNDS
      {
         // PVRTexTool has a bogus KTX writer that doesn't write any imageSize fields. Nice.
         size_t expected_bytes_remaining = 0;
         for (uint mip_level = 0; mip_level < get_num_mips(); mip_level++)
         {
            uint mip_width, mip_height, mip_depth;
            get_mip_dim(mip_level, mip_width, mip_height, mip_depth);

            const uint mip_row_blocks = (mip_width + m_block_dim - 1) / m_block_dim;
            const uint mip_col_blocks = (mip_height + m_block_dim - 1) / m_block_dim;
            if ((!mip_row_blocks) || (!mip_col_blocks))
               return false;

            expected_bytes_remaining += sizeof(uint32);

            if ((!m_header.m_numberOfArrayElements) && (get_num_faces() == 6))
            {
               for (uint face = 0; face < get_num_faces(); face++)
               {
                  uint slice_size = mip_row_blocks * mip_col_blocks * m_bytes_per_block;
                  expected_bytes_remaining += slice_size;

                  uint num_cube_pad_bytes = 3 - ((slice_size + 3) % 4);
                  expected_bytes_remaining += num_cube_pad_bytes;
               }
            }
            else
            {
               uint total_mip_size = 0;
               for (uint array_element = 0; array_element < get_array_size(); array_element++)
               {
                  for (uint face = 0; face < get_num_faces(); face++)
                  {
                     for (uint zslice = 0; zslice < mip_depth; zslice++)
                     {
                        uint slice_size = mip_row_blocks * mip_col_blocks * m_bytes_per_block;
                        total_mip_size += slice_size;
                     }
                  }
               }
               expected_bytes_remaining += total_mip_size;

               uint num_mip_pad_bytes = 3 - ((total_mip_size + 3) % 4);
               expected_bytes_remaining += num_mip_pad_bytes;
            }
         }

         if (serializer.get_stream()->get_remaining() < expected_bytes_remaining)
         {
            has_valid_image_size_fields = false;
            disable_mip_and_cubemap_padding = true;
            console::warning("ktx_texture::read_from_stream: KTX file size is smaller than expected - trying to read anyway without imageSize fields");
         }
      }
#endif

      for (uint mip_level = 0; mip_level < get_num_mips(); mip_level++)
      {
         uint mip_width, mip_height, mip_depth;
         get_mip_dim(mip_level, mip_width, mip_height, mip_depth);

         const uint mip_row_blocks = (mip_width + m_block_dim - 1) / m_block_dim;
         const uint mip_col_blocks = (mip_height + m_block_dim - 1) / m_block_dim;
         if ((!mip_row_blocks) || (!mip_col_blocks))
            return false;

         uint32 image_size = 0;
         if (!has_valid_image_size_fields)
            image_size = mip_depth * mip_row_blocks * mip_col_blocks * m_bytes_per_block * get_array_size() * get_num_faces();
         else
         {
            if (serializer.read(&image_size, 1, sizeof(image_size)) != sizeof(image_size))
               return false;

            if (m_opposite_endianness)
               image_size = utils::swap32(image_size);
         }

         if (!image_size)
            return false;

         uint total_mip_size = 0;

         if ((!m_header.m_numberOfArrayElements) && (get_num_faces() == 6))
         {
            // plain non-array cubemap
            for (uint face = 0; face < get_num_faces(); face++)
            {
               CRNLIB_ASSERT(m_image_data.size() == get_image_index(mip_level, 0, face, 0));

               m_image_data.push_back(uint8_vec());
               uint8_vec& image_data = m_image_data.back();

               image_data.resize(image_size);
               if (serializer.read(&image_data[0], 1, image_size) != image_size)
                  return false;

               if (m_opposite_endianness)
                  utils::endian_swap_mem(&image_data[0], image_size, m_header.m_glTypeSize);

               uint num_cube_pad_bytes = disable_mip_and_cubemap_padding ? 0 : (3 - ((image_size + 3) % 4));
               if (serializer.read(pad_bytes, 1, num_cube_pad_bytes) != num_cube_pad_bytes)
                  return false;

               total_mip_size += image_size + num_cube_pad_bytes;
            }
         }
         else
         {
            // 1D, 2D, 3D (normal or array texture), or array cubemap
            uint num_image_bytes_remaining = image_size;

            for (uint array_element = 0; array_element < get_array_size(); array_element++)
            {
               for (uint face = 0; face < get_num_faces(); face++)
               {
                  for (uint zslice = 0; zslice < mip_depth; zslice++)
                  {
                     CRNLIB_ASSERT(m_image_data.size() == get_image_index(mip_level, array_element, face, zslice));

                     uint slice_size = mip_row_blocks * mip_col_blocks * m_bytes_per_block;
                     if ((!slice_size) || (slice_size > num_image_bytes_remaining))
                        return false;

                     m_image_data.push_back(uint8_vec());
                     uint8_vec& image_data = m_image_data.back();

                     image_data.resize(slice_size);
                     if (serializer.read(&image_data[0], 1, slice_size) != slice_size)
                        return false;

                     if (m_opposite_endianness)
                        utils::endian_swap_mem(&image_data[0], slice_size, m_header.m_glTypeSize);

                     num_image_bytes_remaining -= slice_size;

                     total_mip_size += slice_size;
                  }
               }
            }

            if (num_image_bytes_remaining)
               return false;
         }

         uint num_mip_pad_bytes = disable_mip_and_cubemap_padding ? 0 : (3 - ((total_mip_size + 3) % 4));
         if (serializer.read(pad_bytes, 1, num_mip_pad_bytes) != num_mip_pad_bytes)
            return false;
      }
      return true;
   }

   bool ktx_texture::write_to_stream(data_stream_serializer& serializer, bool no_keyvalue_data)
   {
      if (!consistency_check())
      {
         CRNLIB_ASSERT(0);
         return false;
      }

      memcpy(m_header.m_identifier, s_ktx_file_id, sizeof(m_header.m_identifier));
      m_header.m_endianness = m_opposite_endianness ? KTX_OPPOSITE_ENDIAN : KTX_ENDIAN;

      if (m_block_dim == 1)
      {
         m_header.m_glTypeSize = get_ogl_type_size(m_header.m_glType);
         m_header.m_glBaseInternalFormat = m_header.m_glFormat;
      }
      else
      {
         m_header.m_glBaseInternalFormat = get_ogl_base_internal_fmt(m_header.m_glInternalFormat);
      }

      m_header.m_bytesOfKeyValueData = 0;
      if (!no_keyvalue_data)
      {
         for (uint i = 0; i < m_key_values.size(); i++)
            m_header.m_bytesOfKeyValueData += sizeof(uint32) + ((m_key_values[i].size() + 3) & ~3);
      }

      if (m_opposite_endianness)
         m_header.endian_swap();

      bool success = (serializer.write(&m_header, sizeof(m_header), 1) == 1);

      if (m_opposite_endianness)
         m_header.endian_swap();

      if (!success)
         return success;

      uint total_key_value_bytes = 0;
      const uint8 padding[3] = { 0, 0, 0 };

      if (!no_keyvalue_data)
      {
         for (uint i = 0; i < m_key_values.size(); i++)
         {
            uint32 key_value_size = m_key_values[i].size();

            if (m_opposite_endianness)
               key_value_size = utils::swap32(key_value_size);

            success = (serializer.write(&key_value_size, sizeof(key_value_size), 1) == 1);
            total_key_value_bytes += sizeof(key_value_size);

            if (m_opposite_endianness)
               key_value_size = utils::swap32(key_value_size);

            if (!success)
               return false;

            if (key_value_size)
            {
               if (serializer.write(&m_key_values[i][0], key_value_size, 1) != 1)
                  return false;
               total_key_value_bytes += key_value_size;

               uint num_padding = 3 - ((key_value_size + 3) % 4);
               if ((num_padding) && (serializer.write(padding, num_padding, 1) != 1))
                  return false;
               total_key_value_bytes += num_padding;
            }
         }
         (void)total_key_value_bytes;
      }

      CRNLIB_ASSERT(total_key_value_bytes == m_header.m_bytesOfKeyValueData);

      for (uint mip_level = 0; mip_level < get_num_mips(); mip_level++)
      {
         uint mip_width, mip_height, mip_depth;
         get_mip_dim(mip_level, mip_width, mip_height, mip_depth);

         const uint mip_row_blocks = (mip_width + m_block_dim - 1) / m_block_dim;
         const uint mip_col_blocks = (mip_height + m_block_dim - 1) / m_block_dim;
         if ((!mip_row_blocks) || (!mip_col_blocks))
            return false;

         uint32 image_size = mip_row_blocks * mip_col_blocks * m_bytes_per_block;
         if ((m_header.m_numberOfArrayElements) || (get_num_faces() == 1))
            image_size *= (get_array_size() * get_num_faces() * get_depth());

         if (!image_size)
            return false;

         if (m_opposite_endianness)
            image_size = utils::swap32(image_size);

         success = (serializer.write(&image_size, sizeof(image_size), 1) == 1);

         if (m_opposite_endianness)
            image_size = utils::swap32(image_size);

         if (!success)
            return false;

         uint total_mip_size = 0;

         if ((!m_header.m_numberOfArrayElements) && (get_num_faces() == 6))
         {
            // plain non-array cubemap
            for (uint face = 0; face < get_num_faces(); face++)
            {
               const uint8_vec& image_data = get_image_data(get_image_index(mip_level, 0, face, 0));
               if ((!image_data.size()) || (image_data.size() != image_size))
                  return false;

               if (m_opposite_endianness)
               {
                  uint8_vec tmp_image_data(image_data);
                  utils::endian_swap_mem(&tmp_image_data[0], tmp_image_data.size(), m_header.m_glTypeSize);
                  if (serializer.write(&tmp_image_data[0], tmp_image_data.size(), 1) != 1)
                     return false;
               }
               else if (serializer.write(&image_data[0], image_data.size(), 1) != 1)
                  return false;

               uint num_cube_pad_bytes = 3 - ((image_data.size() + 3) % 4);
               if ((num_cube_pad_bytes) && (serializer.write(padding, num_cube_pad_bytes, 1) != 1))
                  return false;

               total_mip_size += image_size + num_cube_pad_bytes;
            }
         }
         else
         {
            // 1D, 2D, 3D (normal or array texture), or array cubemap
            for (uint array_element = 0; array_element < get_array_size(); array_element++)
            {
               for (uint face = 0; face < get_num_faces(); face++)
               {
                  for (uint zslice = 0; zslice < mip_depth; zslice++)
                  {
                     const uint8_vec& image_data = get_image_data(get_image_index(mip_level, array_element, face, zslice));
                     if (!image_data.size())
                        return false;

                     if (m_opposite_endianness)
                     {
                        uint8_vec tmp_image_data(image_data);
                        utils::endian_swap_mem(&tmp_image_data[0], tmp_image_data.size(), m_header.m_glTypeSize);
                        if (serializer.write(&tmp_image_data[0], tmp_image_data.size(), 1) != 1)
                           return false;
                     }
                     else if (serializer.write(&image_data[0], image_data.size(), 1) != 1)
                        return false;

                     total_mip_size += image_data.size();
                  }
               }
            }

            uint num_mip_pad_bytes = 3 - ((total_mip_size + 3) % 4);
            if ((num_mip_pad_bytes) && (serializer.write(padding, num_mip_pad_bytes, 1) != 1))
               return false;
            total_mip_size += num_mip_pad_bytes;
         }
         CRNLIB_ASSERT((total_mip_size & 3) == 0);
      }

      return true;
   }

   bool ktx_texture::init_2D(uint width, uint height, uint num_mips, uint32 ogl_internal_fmt, uint32 ogl_fmt, uint32 ogl_type)
   {
      clear();

      m_header.m_pixelWidth = width;
      m_header.m_pixelHeight = height;
      m_header.m_numberOfMipmapLevels = num_mips;
      m_header.m_glInternalFormat = ogl_internal_fmt;
      m_header.m_glFormat = ogl_fmt;
      m_header.m_glType = ogl_type;
      m_header.m_numberOfFaces = 1;

      if (!compute_pixel_info())
         return false;

      return true;
   }

   bool ktx_texture::init_2D_array(uint width, uint height, uint num_mips, uint array_size, uint32 ogl_internal_fmt, uint32 ogl_fmt, uint32 ogl_type)
   {
      clear();

      m_header.m_pixelWidth = width;
      m_header.m_pixelHeight = height;
      m_header.m_numberOfMipmapLevels = num_mips;
      m_header.m_numberOfArrayElements = array_size;
      m_header.m_glInternalFormat = ogl_internal_fmt;
      m_header.m_glFormat = ogl_fmt;
      m_header.m_glType = ogl_type;
      m_header.m_numberOfFaces = 1;

      if (!compute_pixel_info())
         return false;

      return true;
   }

   bool ktx_texture::init_3D(uint width, uint height, uint depth, uint num_mips, uint32 ogl_internal_fmt, uint32 ogl_fmt, uint32 ogl_type)
   {
      clear();

      m_header.m_pixelWidth = width;
      m_header.m_pixelHeight = height;
      m_header.m_pixelDepth = depth;
      m_header.m_numberOfMipmapLevels = num_mips;
      m_header.m_glInternalFormat = ogl_internal_fmt;
      m_header.m_glFormat = ogl_fmt;
      m_header.m_glType = ogl_type;
      m_header.m_numberOfFaces = 1;

      if (!compute_pixel_info())
         return false;

      return true;
   }

   bool ktx_texture::init_cubemap(uint dim, uint num_mips, uint32 ogl_internal_fmt, uint32 ogl_fmt, uint32 ogl_type)
   {
      clear();

      m_header.m_pixelWidth = dim;
      m_header.m_pixelHeight = dim;
      m_header.m_numberOfMipmapLevels = num_mips;
      m_header.m_glInternalFormat = ogl_internal_fmt;
      m_header.m_glFormat = ogl_fmt;
      m_header.m_glType = ogl_type;
      m_header.m_numberOfFaces = 6;

      if (!compute_pixel_info())
         return false;

      return true;
   }

   bool ktx_texture::check_header() const
   {
      if (((get_num_faces() != 1) && (get_num_faces() != 6)) || (!m_header.m_pixelWidth))
         return false;

      if ((!m_header.m_pixelHeight) && (m_header.m_pixelDepth))
         return false;

      if ((get_num_faces() == 6) && ((m_header.m_pixelDepth) || (!m_header.m_pixelHeight)))
         return false;

      if (m_header.m_numberOfMipmapLevels)
      {
         const uint max_mipmap_dimension = 1U << (m_header.m_numberOfMipmapLevels - 1U);
         if (max_mipmap_dimension > (CRNLIB_MAX(CRNLIB_MAX(m_header.m_pixelWidth, m_header.m_pixelHeight), m_header.m_pixelDepth)))
            return false;
      }

      return true;
   }

   bool ktx_texture::consistency_check() const
   {
      if (!check_header())
         return false;

      uint block_dim = 0, bytes_per_block = 0;
      if ((!m_header.m_glType) || (!m_header.m_glFormat))
      {
         if ((m_header.m_glType) || (m_header.m_glFormat))
            return false;
         if (!get_ogl_fmt_desc(m_header.m_glInternalFormat, m_header.m_glType, block_dim, bytes_per_block))
            return false;
         if (block_dim == 1)
            return false;
         //if ((get_width() % block_dim) || (get_height() % block_dim))
         //   return false;
      }
      else
      {
         if (!get_ogl_fmt_desc(m_header.m_glFormat, m_header.m_glType, block_dim, bytes_per_block))
            return false;
         if (block_dim > 1)
            return false;
      }
      if ((m_block_dim != block_dim) || (m_bytes_per_block != bytes_per_block))
         return false;

      if (m_image_data.size() != get_total_images())
         return false;

      for (uint mip_level = 0; mip_level < get_num_mips(); mip_level++)
      {
         uint mip_width, mip_height, mip_depth;
         get_mip_dim(mip_level, mip_width, mip_height, mip_depth);

         const uint mip_row_blocks = (mip_width + m_block_dim - 1) / m_block_dim;
         const uint mip_col_blocks = (mip_height + m_block_dim - 1) / m_block_dim;
         if ((!mip_row_blocks) || (!mip_col_blocks))
            return false;

         for (uint array_element = 0; array_element < get_array_size(); array_element++)
         {
            for (uint face = 0; face < get_num_faces(); face++)
            {
               for (uint zslice = 0; zslice < mip_depth; zslice++)
               {
                  const uint8_vec& image_data = get_image_data(get_image_index(mip_level, array_element, face, zslice));

                  uint expected_image_size = mip_row_blocks * mip_col_blocks * m_bytes_per_block;
                  if (image_data.size() != expected_image_size)
                     return false;
               }
            }
         }
      }

      return true;
   }

   const uint8_vec* ktx_texture::find_key(const char* pKey) const
   {
      const size_t n = strlen(pKey) + 1;
      for (uint i = 0; i < m_key_values.size(); i++)
      {
         const uint8_vec& v = m_key_values[i];
         if ((v.size() >= n) && (!memcmp(&v[0], pKey, n)))
            return &v;
      }

      return NULL;
   }

   bool ktx_texture::get_key_value_as_string(const char* pKey, dynamic_string& str) const
   {
      const uint8_vec* p = find_key(pKey);
      if (!p)
      {
         str.clear();
         return false;
      }

      const uint ofs = (static_cast<uint>(strlen(pKey)) + 1);
      const uint8* pValue = p->get_ptr() + ofs;
      const uint n = p->size() - ofs;

      uint i;
      for (i = 0; i < n; i++)
         if (!pValue[i])
            break;

      str.set_from_buf(pValue, i);
      return true;
   }

   uint ktx_texture::add_key_value(const char* pKey, const void* pVal, uint val_size)
   {
      const uint idx = m_key_values.size();
      m_key_values.resize(idx + 1);
      uint8_vec& v = m_key_values.back();
      v.append(reinterpret_cast<const uint8*>(pKey), static_cast<uint>(strlen(pKey)) + 1);
      v.append(static_cast<const uint8*>(pVal), val_size);
      return idx;
   }

} // namespace crnlib