// File: crn_dds_texture.cpp - Actually supports both .DDS and .KTX. Probably will rename this eventually.
// See Copyright Notice and license at the end of inc/crnlib.h
#include "crn_core.h"
#include "crn_mipmapped_texture.h"
#include "crn_cfile_stream.h"
#include "crn_image_utils.h"
#include "crn_console.h"
#include "crn_texture_comp.h"
#include "crn_ktx_texture.h"

#include "../inc/crn_defs.h"

namespace crnlib {
const vec2I g_vertical_cross_image_offsets[6] = {vec2I(2, 1), vec2I(0, 1), vec2I(1, 0), vec2I(1, 2), vec2I(1, 1), vec2I(1, 3)};

mip_level::mip_level()
    : m_width(0),
      m_height(0),
      m_comp_flags(pixel_format_helpers::cDefaultCompFlags),
      m_format(PIXEL_FMT_INVALID),
      m_pImage(NULL),
      m_pDXTImage(NULL),
      m_orient_flags(cDefaultOrientationFlags) {
}

mip_level::mip_level(const mip_level& other)
    : m_width(0),
      m_height(0),
      m_comp_flags(pixel_format_helpers::cDefaultCompFlags),
      m_format(PIXEL_FMT_INVALID),
      m_pImage(NULL),
      m_pDXTImage(NULL),
      m_orient_flags(cDefaultOrientationFlags) {
  *this = other;
}

mip_level& mip_level::operator=(const mip_level& rhs) {
  clear();

  m_width = rhs.m_width;
  m_height = rhs.m_height;
  m_comp_flags = rhs.m_comp_flags;
  m_format = rhs.m_format;
  m_orient_flags = rhs.m_orient_flags;

  if (rhs.m_pImage)
    m_pImage = crnlib_new<image_u8>(*rhs.m_pImage);

  if (rhs.m_pDXTImage)
    m_pDXTImage = crnlib_new<dxt_image>(*rhs.m_pDXTImage);

  return *this;
}

mip_level::~mip_level() {
  crnlib_delete(m_pImage);
  crnlib_delete(m_pDXTImage);
}

void mip_level::clear() {
  m_width = 0;
  m_height = 0;
  m_comp_flags = pixel_format_helpers::cDefaultCompFlags;
  m_format = PIXEL_FMT_INVALID;
  m_orient_flags = cDefaultOrientationFlags;

  if (m_pImage) {
    crnlib_delete(m_pImage);
    m_pImage = NULL;
  }

  if (m_pDXTImage) {
    crnlib_delete(m_pDXTImage);
    m_pDXTImage = NULL;
  }
}

void mip_level::assign(image_u8* p, pixel_format fmt, orientation_flags_t orient_flags) {
  CRNLIB_ASSERT(p);

  clear();

  m_pImage = p;

  m_width = p->get_width();
  m_height = p->get_height();
  m_orient_flags = orient_flags;

  if (fmt != PIXEL_FMT_INVALID)
    m_format = fmt;
  else {
    if (p->is_grayscale())
      m_format = p->is_component_valid(3) ? PIXEL_FMT_A8L8 : PIXEL_FMT_L8;
    else
      m_format = p->is_component_valid(3) ? PIXEL_FMT_A8R8G8B8 : PIXEL_FMT_R8G8B8;
  }

  m_comp_flags = p->get_comp_flags();  //pixel_format_helpers::get_component_flags(m_format);
}

void mip_level::assign(dxt_image* p, pixel_format fmt, orientation_flags_t orient_flags) {
  CRNLIB_ASSERT(p);

  clear();

  m_pDXTImage = p;

  m_width = p->get_width();
  m_height = p->get_height();
  m_orient_flags = orient_flags;

  if (fmt != PIXEL_FMT_INVALID)
    m_format = fmt;
  else
    m_format = pixel_format_helpers::from_dxt_format(p->get_format());

  m_comp_flags = pixel_format_helpers::get_component_flags(m_format);
}

bool mip_level::pack_to_dxt(const image_u8& img, pixel_format fmt, bool cook, const dxt_image::pack_params& orig_params, orientation_flags_t orient_flags) {
  CRNLIB_ASSERT(pixel_format_helpers::is_dxt(fmt));
  if (!pixel_format_helpers::is_dxt(fmt))
    return false;

  dxt_image::pack_params p(orig_params);
  if (pixel_format_helpers::is_pixel_format_non_srgb(fmt) || (img.get_comp_flags() & pixel_format_helpers::cCompFlagNormalMap) || (img.get_comp_flags() & pixel_format_helpers::cCompFlagLumaChroma)) {
    // Disable perceptual colorspace metrics when packing to swizzled or non-RGB pixel formats.
    p.m_perceptual = false;
  }

  image_u8 tmp_img(img);

  clear();

  m_format = fmt;

  if (cook)
    cook_image(tmp_img);

  if ((pixel_format_helpers::is_alpha_only(fmt)) && (!tmp_img.has_alpha()))
    tmp_img.set_alpha_to_luma();

  dxt_format dxt_fmt = pixel_format_helpers::get_dxt_format(fmt);

  dxt_image* pDXT_image = crnlib_new<dxt_image>();
  if (!pDXT_image->init(dxt_fmt, tmp_img, p)) {
    clear();
    return false;
  }

  assign(pDXT_image, fmt, orient_flags);

  return true;
}

bool mip_level::pack_to_dxt(pixel_format fmt, bool cook, const dxt_image::pack_params& p) {
  CRNLIB_ASSERT(pixel_format_helpers::is_dxt(fmt));
  if (!pixel_format_helpers::is_dxt(fmt))
    return false;

  image_u8 tmp_img;
  image_u8* pImage = get_unpacked_image(tmp_img, cUnpackFlagUncook);

  return pack_to_dxt(*pImage, fmt, cook, p, m_orient_flags);
}

bool mip_level::unpack_from_dxt(bool uncook) {
  if (!m_pDXTImage)
    return false;

  image_u8* pNew_img = crnlib_new<image_u8>();
  CRNLIB_ASSERT(get_unpacked_image(*pNew_img, uncook ? cUnpackFlagUncook : 0) == pNew_img);

  assign(pNew_img, PIXEL_FMT_INVALID, m_orient_flags);
  return true;
}

bool mip_level::is_flipped() const {
  return ((m_orient_flags & (cOrientationFlagXFlipped | cOrientationFlagYFlipped)) != 0);
}

bool mip_level::is_x_flipped() const {
  return ((m_orient_flags & cOrientationFlagXFlipped) != 0);
}

bool mip_level::is_y_flipped() const {
  return ((m_orient_flags & cOrientationFlagYFlipped) != 0);
}

bool mip_level::can_unflip_without_unpacking() const {
  if (!is_valid())
    return false;

  if (!is_packed())
    return true;

  bool can_unflip = true;
  if (m_orient_flags & cOrientationFlagXFlipped) {
    if (!m_pDXTImage->can_flip(0))
      can_unflip = false;
  }
  if (m_orient_flags & cOrientationFlagYFlipped) {
    if (!m_pDXTImage->can_flip(1))
      can_unflip = false;
  }

  return can_unflip;
}

bool mip_level::unflip(bool allow_unpacking_to_flip, bool uncook_if_necessary_to_unpack) {
  if (!is_valid())
    return false;

  if (!is_flipped())
    return false;

  if (is_packed()) {
    if (can_unflip_without_unpacking()) {
      if (m_orient_flags & cOrientationFlagXFlipped) {
        m_pDXTImage->flip_x();
        m_orient_flags = static_cast<orientation_flags_t>(m_orient_flags & ~cOrientationFlagXFlipped);
      }

      if (m_orient_flags & cOrientationFlagYFlipped) {
        m_pDXTImage->flip_y();
        m_orient_flags = static_cast<orientation_flags_t>(m_orient_flags & ~cOrientationFlagYFlipped);
      }

      return true;
    }

    if (!allow_unpacking_to_flip)
      return false;
  }

  unpack_from_dxt(uncook_if_necessary_to_unpack);

  if (m_orient_flags & cOrientationFlagXFlipped) {
    m_pImage->flip_x();
    m_orient_flags = static_cast<orientation_flags_t>(m_orient_flags & ~cOrientationFlagXFlipped);
  }

  if (m_orient_flags & cOrientationFlagYFlipped) {
    m_pImage->flip_y();
    m_orient_flags = static_cast<orientation_flags_t>(m_orient_flags & ~cOrientationFlagYFlipped);
  }

  return true;
}

bool mip_level::set_alpha_to_luma() {
  if (m_pDXTImage)
    unpack_from_dxt(true);

  m_pImage->set_alpha_to_luma();

  m_comp_flags = m_pImage->get_comp_flags();

  if (m_pImage->is_grayscale())
    m_format = PIXEL_FMT_A8L8;
  else
    m_format = PIXEL_FMT_A8R8G8B8;

  return true;
}

bool mip_level::convert(image_utils::conversion_type conv_type) {
  if (m_pDXTImage)
    unpack_from_dxt(true);

  image_utils::convert_image(*m_pImage, conv_type);

  m_comp_flags = m_pImage->get_comp_flags();

  if (m_pImage->is_grayscale())
    m_format = m_pImage->has_alpha() ? PIXEL_FMT_A8L8 : PIXEL_FMT_L8;
  else
    m_format = m_pImage->has_alpha() ? PIXEL_FMT_A8R8G8B8 : PIXEL_FMT_R8G8B8;

  return true;
}

bool mip_level::convert(pixel_format fmt, bool cook, const dxt_image::pack_params& p) {
  if (pixel_format_helpers::is_dxt(fmt))
    return pack_to_dxt(fmt, cook, p);

  image_u8 tmp_img;
  image_u8* pImg = get_unpacked_image(tmp_img, cUnpackFlagUncook);

  image_u8* pImage = crnlib_new<image_u8>();
  pImage->set_comp_flags(pixel_format_helpers::get_component_flags(fmt));

  if (!pImage->resize(pImg->get_width(), pImg->get_height()))
    return false;

  for (uint y = 0; y < pImg->get_height(); y++) {
    for (uint x = 0; x < pImg->get_width(); x++) {
      color_quad_u8 c((*pImg)(x, y));

      if ((pixel_format_helpers::is_alpha_only(fmt)) && (!pImg->has_alpha())) {
        c.a = static_cast<uint8>(c.get_luma());
      } else {
        if (pImage->is_grayscale()) {
          uint8 g = static_cast<uint8>(c.get_luma());
          c.r = g;
          c.g = g;
          c.b = g;
        }

        if (!pImage->is_component_valid(3))
          c.a = 255;
      }

      (*pImage)(x, y) = c;
    }
  }

  assign(pImage, fmt, m_orient_flags);

  return true;
}

void mip_level::cook_image(image_u8& img) const {
  image_utils::conversion_type conv_type = image_utils::get_conversion_type(true, m_format);

  if (conv_type != image_utils::cConversion_Invalid)
    image_utils::convert_image(img, conv_type);
}

void mip_level::uncook_image(image_u8& img) const {
  image_utils::conversion_type conv_type = image_utils::get_conversion_type(false, m_format);

  if (conv_type != image_utils::cConversion_Invalid)
    image_utils::convert_image(img, conv_type);
}

image_u8* mip_level::get_unpacked_image(image_u8& tmp, uint unpack_flags) const {
  if (!is_valid())
    return NULL;

  if (m_pDXTImage) {
    m_pDXTImage->unpack(tmp);

    tmp.set_comp_flags(m_comp_flags);

    if (unpack_flags & cUnpackFlagUncook)
      uncook_image(tmp);
  } else if ((unpack_flags & cUnpackFlagUnflip) && (m_orient_flags & (cOrientationFlagXFlipped | cOrientationFlagYFlipped)))
    tmp = *m_pImage;
  else
    return m_pImage;

  if (unpack_flags & cUnpackFlagUnflip) {
    if (m_orient_flags & cOrientationFlagXFlipped)
      tmp.flip_x();
    if (m_orient_flags & cOrientationFlagYFlipped)
      tmp.flip_y();
  }

  return &tmp;
}

bool mip_level::flip_x() {
  if (!is_valid())
    return false;

  if (m_pDXTImage)
    return m_pDXTImage->flip_x();
  else if (m_pImage) {
    m_pImage->flip_x();
    return true;
  }

  return false;
}

bool mip_level::flip_y() {
  if (!is_valid())
    return false;

  if (m_pDXTImage)
    return m_pDXTImage->flip_y();
  else if (m_pImage) {
    m_pImage->flip_y();
    return true;
  }

  return false;
}

// -------------------------------------------------------------------------

mipmapped_texture::mipmapped_texture()
    : m_width(0),
      m_height(0),
      m_comp_flags(pixel_format_helpers::cDefaultCompFlags),
      m_format(PIXEL_FMT_INVALID),
      m_source_file_type(texture_file_types::cFormatInvalid) {
}

mipmapped_texture::~mipmapped_texture() {
  free_all_mips();
}

void mipmapped_texture::clear() {
  free_all_mips();

  m_name.clear();
  m_width = 0;
  m_height = 0;
  m_comp_flags = pixel_format_helpers::cDefaultCompFlags;
  m_format = PIXEL_FMT_INVALID;
  m_source_file_type = texture_file_types::cFormatInvalid;
  m_last_error.clear();
}

void mipmapped_texture::free_all_mips() {
  for (uint i = 0; i < m_faces.size(); i++)
    for (uint j = 0; j < m_faces[i].size(); j++)
      crnlib_delete(m_faces[i][j]);

  m_faces.clear();
}

mipmapped_texture::mipmapped_texture(const mipmapped_texture& other)
    : m_width(0),
      m_height(0),
      m_comp_flags(pixel_format_helpers::cDefaultCompFlags),
      m_format(PIXEL_FMT_INVALID) {
  *this = other;
}

mipmapped_texture& mipmapped_texture::operator=(const mipmapped_texture& rhs) {
  if (this == &rhs)
    return *this;

  clear();

  m_name = rhs.m_name;
  m_width = rhs.m_width;
  m_height = rhs.m_height;

  m_comp_flags = rhs.m_comp_flags;
  m_format = rhs.m_format;

  m_faces.resize(rhs.m_faces.size());
  for (uint i = 0; i < m_faces.size(); i++) {
    m_faces[i].resize(rhs.m_faces[i].size());

    for (uint j = 0; j < rhs.m_faces[i].size(); j++)
      m_faces[i][j] = crnlib_new<mip_level>(*rhs.m_faces[i][j]);
  }

  CRNLIB_ASSERT((!is_valid()) || check());

  return *this;
}

bool mipmapped_texture::read_dds(data_stream_serializer& serializer) {
  if (!read_dds_internal(serializer)) {
    clear();
    return false;
  }

  return true;
}

bool mipmapped_texture::read_dds_internal(data_stream_serializer& serializer) {
  CRNLIB_ASSERT(serializer.get_little_endian());

  clear();

  set_last_error("Not a DDS file");

  uint8 hdr[4];
  if (!serializer.read(hdr, sizeof(hdr)))
    return false;

  if (memcmp(hdr, "DDS ", 4) != 0)
    return false;

  DDSURFACEDESC2 desc;
  if (!serializer.read(&desc, sizeof(desc)))
    return false;

  if (!c_crnlib_little_endian_platform)
    utils::endian_switch_dwords(reinterpret_cast<uint32*>(&desc), sizeof(desc) / sizeof(uint32));

  if (desc.dwSize != sizeof(desc))
    return false;

  if ((!desc.dwHeight) || (!desc.dwWidth) || (desc.dwHeight > cDDSMaxImageDimensions) || (desc.dwWidth > cDDSMaxImageDimensions))
    return false;

  m_width = desc.dwWidth;
  m_height = desc.dwHeight;

  uint num_mip_levels = 1;

  if ((desc.dwFlags & DDSD_MIPMAPCOUNT) && (desc.ddsCaps.dwCaps & DDSCAPS_MIPMAP) && (desc.dwMipMapCount)) {
    num_mip_levels = desc.dwMipMapCount;
    if (num_mip_levels > utils::compute_max_mips(desc.dwWidth, desc.dwHeight))
      return false;
  }

  uint num_faces = 1;

  if (desc.ddsCaps.dwCaps & DDSCAPS_COMPLEX) {
    if (desc.ddsCaps.dwCaps2 & DDSCAPS2_CUBEMAP) {
      const uint all_faces_mask = DDSCAPS2_CUBEMAP_POSITIVEX | DDSCAPS2_CUBEMAP_NEGATIVEX | DDSCAPS2_CUBEMAP_POSITIVEY | DDSCAPS2_CUBEMAP_NEGATIVEY | DDSCAPS2_CUBEMAP_POSITIVEZ | DDSCAPS2_CUBEMAP_NEGATIVEZ;
      if ((desc.ddsCaps.dwCaps2 & all_faces_mask) != all_faces_mask) {
        set_last_error("Incomplete cubemaps unsupported");
        return false;
      }

      num_faces = 6;
    } else if (desc.ddsCaps.dwCaps2 & DDSCAPS2_VOLUME) {
      set_last_error("Volume textures unsupported");
      return false;
    }
  }

  if (desc.ddpfPixelFormat.dwFlags & DDPF_PALETTEINDEXED8) {
    // It's difficult to even make P8 textures with existing tools:
    // nvdxt just hangs
    // dxtex.exe just makes all-white textures
    // So screw it.
    set_last_error("Palettized textures unsupported");
    return false;
  }

  dxt_format dxt_fmt = cDXTInvalid;

  if (desc.ddpfPixelFormat.dwFlags & DDPF_FOURCC) {
    // http://code.google.com/p/nvidia-texture-tools/issues/detail?id=41
    // ATI2 YX:            0 (0x00000000)
    // ATI2 XY:   1498952257 (0x59583241) (BC5)
    // ATI Compressonator obeys this stuff, nvidia's tools (like readdxt) don't - oh great

    switch (desc.ddpfPixelFormat.dwFourCC) {
      case PIXEL_FMT_DXT1: {
        m_format = PIXEL_FMT_DXT1;
        dxt_fmt = cDXT1;
        break;
      }
      case PIXEL_FMT_DXT2:
      case PIXEL_FMT_DXT3: {
        m_format = PIXEL_FMT_DXT3;
        dxt_fmt = cDXT3;
        break;
      }
      case PIXEL_FMT_DXT4:
      case PIXEL_FMT_DXT5: {
        switch (desc.ddpfPixelFormat.dwRGBBitCount) {
          case PIXEL_FMT_DXT5_CCxY:
            m_format = PIXEL_FMT_DXT5_CCxY;
            break;
          case PIXEL_FMT_DXT5_xGxR:
            m_format = PIXEL_FMT_DXT5_xGxR;
            break;
          case PIXEL_FMT_DXT5_xGBR:
            m_format = PIXEL_FMT_DXT5_xGBR;
            break;
          case PIXEL_FMT_DXT5_AGBR:
            m_format = PIXEL_FMT_DXT5_AGBR;
            break;
          default:
            m_format = PIXEL_FMT_DXT5;
            break;
        }

        dxt_fmt = cDXT5;
        break;
      }
      case PIXEL_FMT_3DC: {
        if (desc.ddpfPixelFormat.dwRGBBitCount == CRNLIB_PIXEL_FMT_FOURCC('A', '2', 'X', 'Y')) {
          dxt_fmt = cDXN_XY;
          m_format = PIXEL_FMT_DXN;
        } else {
          dxt_fmt = cDXN_YX;  // aka ATI2
          m_format = PIXEL_FMT_3DC;
        }

        break;
      }
      case PIXEL_FMT_DXT5A: {
        m_format = PIXEL_FMT_DXT5A;
        dxt_fmt = cDXT5A;
        break;
      }
      case PIXEL_FMT_ETC1: {
        m_format = PIXEL_FMT_ETC1;
        dxt_fmt = cETC1;
        break;
      }
      case PIXEL_FMT_ETC2: {
        m_format = PIXEL_FMT_ETC2;
        dxt_fmt = cETC2;
        break;
      }
      case PIXEL_FMT_ETC2A: {
        m_format = PIXEL_FMT_ETC2A;
        dxt_fmt = cETC2A;
        break;
      }
      case PIXEL_FMT_ETC1S: {
        m_format = PIXEL_FMT_ETC1S;
        dxt_fmt = cETC1S;
        break;
      }
      case PIXEL_FMT_ETC2AS: {
        m_format = PIXEL_FMT_ETC2AS;
        dxt_fmt = cETC2AS;
        break;
      }
      default: {
        dynamic_string err_msg(cVarArg, "Unsupported DDS FOURCC format: 0x%08X", desc.ddpfPixelFormat.dwFourCC);
        set_last_error(err_msg.get_ptr());
        return false;
      }
    }
  } else if ((desc.ddpfPixelFormat.dwRGBBitCount < 8) || (desc.ddpfPixelFormat.dwRGBBitCount > 32) || (desc.ddpfPixelFormat.dwRGBBitCount & 7)) {
    set_last_error("Unsupported bit count");
    return false;
  } else if (desc.ddpfPixelFormat.dwFlags & DDPF_RGB) {
    if (desc.ddpfPixelFormat.dwFlags & DDPF_LUMINANCE) {
      if (desc.ddpfPixelFormat.dwFlags & DDPF_ALPHAPIXELS)
        m_format = PIXEL_FMT_A8L8;
      else
        m_format = PIXEL_FMT_L8;
    } else if (desc.ddpfPixelFormat.dwFlags & DDPF_ALPHAPIXELS)
      m_format = PIXEL_FMT_A8R8G8B8;
    else
      m_format = PIXEL_FMT_R8G8B8;
  } else if (desc.ddpfPixelFormat.dwFlags & DDPF_ALPHAPIXELS) {
    if (desc.ddpfPixelFormat.dwFlags & DDPF_LUMINANCE)
      m_format = PIXEL_FMT_A8L8;
    else
      m_format = PIXEL_FMT_A8;
  } else if (desc.ddpfPixelFormat.dwFlags & DDPF_LUMINANCE) {
    m_format = PIXEL_FMT_L8;
  } else if (desc.ddpfPixelFormat.dwFlags & DDPF_ALPHA) {
    m_format = PIXEL_FMT_A8;
  } else {
    set_last_error("Unsupported format");
    return false;
  }

  m_comp_flags = pixel_format_helpers::get_component_flags(m_format);

  uint bits_per_pixel = desc.ddpfPixelFormat.dwRGBBitCount;

  if (desc.ddpfPixelFormat.dwFlags & DDPF_FOURCC)
    bits_per_pixel = pixel_format_helpers::get_bpp(m_format);

  set_last_error("Load failed");

  uint default_pitch;
  if (desc.ddpfPixelFormat.dwFlags & DDPF_FOURCC)
    default_pitch = (((desc.dwWidth + 3) & ~3) * ((desc.dwHeight + 3) & ~3) * bits_per_pixel) >> 3;
  else
    default_pitch = (desc.dwWidth * bits_per_pixel) >> 3;

  uint pitch = 0;
  if ((desc.dwFlags & DDSD_PITCH) && (!(desc.dwFlags & DDSD_LINEARSIZE))) {
    pitch = desc.lPitch;
  }

  if (!pitch)
    pitch = default_pitch;
#if 0
      else if (pitch & 3)
      {
         // MS's DDS docs say the pitch must be DWORD aligned - but this isn't always the case.
         // ATI Compressonator writes images with non-DWORD aligned pitches, and the DDSWithoutD3DX sample from MS doesn't compute the proper DWORD aligned pitch when reading DDS
         // files, so the docs must be wrong/outdated.
         console::warning("DDS file's pitch is not divisible by 4 - trying to load anyway.");
      }
#endif
  // Check for obviously wacky source pitches (probably a corrupted/invalid file).
  else if (pitch > default_pitch * 8) {
    set_last_error("Invalid pitch");
    return false;
  }

  crnlib::vector<uint8> load_buf;

  uint mask_size[4];
  mask_size[0] = math::bitmask_size(desc.ddpfPixelFormat.dwRBitMask);
  mask_size[1] = math::bitmask_size(desc.ddpfPixelFormat.dwGBitMask);
  mask_size[2] = math::bitmask_size(desc.ddpfPixelFormat.dwBBitMask);
  mask_size[3] = math::bitmask_size(desc.ddpfPixelFormat.dwRGBAlphaBitMask);

  uint mask_ofs[4];
  mask_ofs[0] = math::bitmask_ofs(desc.ddpfPixelFormat.dwRBitMask);
  mask_ofs[1] = math::bitmask_ofs(desc.ddpfPixelFormat.dwGBitMask);
  mask_ofs[2] = math::bitmask_ofs(desc.ddpfPixelFormat.dwBBitMask);
  mask_ofs[3] = math::bitmask_ofs(desc.ddpfPixelFormat.dwRGBAlphaBitMask);

  if ((desc.ddpfPixelFormat.dwFlags & DDPF_LUMINANCE) && (!mask_size[0])) {
    mask_size[0] = desc.ddpfPixelFormat.dwRGBBitCount >> 3;
    if (desc.ddpfPixelFormat.dwFlags & DDPF_ALPHAPIXELS)
      mask_size[0] /= 2;
  }

  m_faces.resize(num_faces);

  bool dxt1_alpha = false;

  for (uint face_index = 0; face_index < num_faces; face_index++) {
    m_faces[face_index].resize(num_mip_levels);

    for (uint level_index = 0; level_index < num_mip_levels; level_index++) {
      const uint width = math::maximum<uint>(desc.dwWidth >> level_index, 1U);
      const uint height = math::maximum<uint>(desc.dwHeight >> level_index, 1U);

      mip_level* pMip = crnlib_new<mip_level>();
      m_faces[face_index][level_index] = pMip;

      if (desc.ddpfPixelFormat.dwFlags & DDPF_FOURCC) {
        const uint bytes_per_block = pixel_format_helpers::get_dxt_bytes_per_block(m_format);

        const uint num_blocks_x = (width + 3) >> 2;
        const uint num_blocks_y = (height + 3) >> 2;

        const uint actual_level_pitch = num_blocks_x * num_blocks_y * bytes_per_block;
        const uint level_pitch = level_index ? actual_level_pitch : pitch;

        dxt_image* pDXTImage = crnlib_new<dxt_image>();
        if (!pDXTImage->init(dxt_fmt, width, height, false)) {
          crnlib_delete(pDXTImage);

          CRNLIB_ASSERT(0);
          return false;
        }

        CRNLIB_ASSERT(pDXTImage->get_element_vec().size() * sizeof(dxt_image::element) == actual_level_pitch);

        if (!serializer.read(&pDXTImage->get_element_vec()[0], actual_level_pitch)) {
          crnlib_delete(pDXTImage);

          return false;
        }

        // DDS image in memory are always assumed to be little endian - the same as DDS itself.
        //if (c_crnlib_big_endian_platform)
        //   utils::endian_switch_words(reinterpret_cast<uint16*>(&pDXTImage->get_element_vec()[0]), actual_level_pitch / sizeof(uint16));

        if (level_pitch > actual_level_pitch) {
          if (!serializer.skip(level_pitch - actual_level_pitch)) {
            crnlib_delete(pDXTImage);

            return false;
          }
        }

        if ((m_format == PIXEL_FMT_DXT1) && (!dxt1_alpha))
          dxt1_alpha = pDXTImage->has_alpha();

        pMip->assign(pDXTImage, m_format);
      } else {
        image_u8* pImage = crnlib_new<image_u8>(width, height);

        pImage->set_comp_flags(m_comp_flags);

        const uint bytes_per_pixel = desc.ddpfPixelFormat.dwRGBBitCount >> 3;
        const uint actual_line_pitch = width * bytes_per_pixel;
        const uint line_pitch = level_index ? actual_line_pitch : pitch;

        if (load_buf.size() < line_pitch)
          load_buf.resize(line_pitch);

        color_quad_u8 q(0, 0, 0, 255);

        for (uint y = 0; y < height; y++) {
          if (!serializer.read(&load_buf[0], line_pitch)) {
            crnlib_delete(pImage);
            return false;
          }

          color_quad_u8* pDst = pImage->get_scanline(y);

          for (uint x = 0; x < width; x++) {
            const uint8* pPixel = &load_buf[x * bytes_per_pixel];

            uint c = 0;
            // Assumes DDS is always little endian.
            for (uint l = 0; l < bytes_per_pixel; l++)
              c |= (pPixel[l] << (l * 8U));

            for (uint i = 0; i < 4; i++) {
              if (!mask_size[i])
                continue;

              uint mask = (1U << mask_size[i]) - 1U;
              uint bits = (c >> mask_ofs[i]) & mask;

              uint v = (bits * 255 + (mask >> 1)) / mask;

              q.set_component(i, v);
            }

            if (desc.ddpfPixelFormat.dwFlags & DDPF_LUMINANCE) {
              q.g = q.r;
              q.b = q.r;
            }

            *pDst++ = q;
          }
        }

        pMip->assign(pImage, m_format);

        CRNLIB_ASSERT(pMip->get_comp_flags() == m_comp_flags);
      }
    }
  }

  clear_last_error();

  if (dxt1_alpha)
    change_dxt1_to_dxt1a();

  return true;
}

void mipmapped_texture::change_dxt1_to_dxt1a() {
  if (m_format != PIXEL_FMT_DXT1)
    return;

  m_format = PIXEL_FMT_DXT1A;

  m_comp_flags = pixel_format_helpers::get_component_flags(m_format);

  for (uint f = 0; f < m_faces.size(); f++) {
    for (uint l = 0; l < m_faces[f].size(); l++) {
      if (m_faces[f][l]->get_dxt_image()) {
        m_faces[f][l]->set_format(m_format);
        m_faces[f][l]->set_comp_flags(m_comp_flags);

        m_faces[f][l]->get_dxt_image()->change_dxt1_to_dxt1a();
      }
    }
  }
}

bool mipmapped_texture::check() const {
  uint levels = 0;
  orientation_flags_t orient_flags = cDefaultOrientationFlags;
  for (uint f = 0; f < m_faces.size(); f++) {
    if (!f) {
      levels = m_faces[f].size();
      if ((levels) && (m_faces[f][0]))
        orient_flags = m_faces[f][0]->get_orientation_flags();
    } else if (m_faces[f].size() != levels)
      return false;

    for (uint l = 0; l < m_faces[f].size(); l++) {
      mip_level* p = m_faces[f][l];
      if (!p)
        return false;

      if (!p->is_valid())
        return false;

      if (p->get_orientation_flags() != orient_flags)
        return false;

      if (!l) {
        if (m_width != p->get_width())
          return false;
        if (m_height != p->get_height())
          return false;
      }

      if (p->get_comp_flags() != m_comp_flags)
        return false;

      if (p->get_format() != m_format)
        return false;

      if (p->get_image()) {
        if (pixel_format_helpers::is_dxt(p->get_format()))
          return false;

        if (p->get_image()->get_width() != p->get_width())
          return false;
        if (p->get_image()->get_height() != p->get_height())
          return false;
        if (p->get_image()->get_comp_flags() != m_comp_flags)
          return false;
      } else if (!pixel_format_helpers::is_dxt(p->get_format()))
        return false;
    }
  }

  return true;
}

bool mipmapped_texture::write_dds(data_stream_serializer& serializer) const {
  if (!m_width) {
    set_last_error("Nothing to write");
    return false;
  }

  set_last_error("write_dds() failed");

  if (!serializer.write("DDS ", sizeof(uint32)))
    return false;

  DDSURFACEDESC2 desc;
  utils::zero_object(desc);

  desc.dwSize = sizeof(desc);
  desc.dwFlags = DDSD_WIDTH | DDSD_HEIGHT | DDSD_PIXELFORMAT | DDSD_CAPS;

  desc.dwWidth = m_width;
  desc.dwHeight = m_height;

  desc.ddsCaps.dwCaps = DDSCAPS_TEXTURE;
  desc.ddpfPixelFormat.dwSize = sizeof(desc.ddpfPixelFormat);

  if (get_num_levels() > 1) {
    desc.dwMipMapCount = get_num_levels();
    desc.dwFlags |= DDSD_MIPMAPCOUNT;
    desc.ddsCaps.dwCaps |= (DDSCAPS_MIPMAP | DDSCAPS_COMPLEX);
  }

  if (get_num_faces() > 1) {
    desc.ddsCaps.dwCaps |= DDSCAPS_COMPLEX;
    desc.ddsCaps.dwCaps2 |= DDSCAPS2_CUBEMAP;
    desc.ddsCaps.dwCaps2 |= DDSCAPS2_CUBEMAP_POSITIVEX | DDSCAPS2_CUBEMAP_NEGATIVEX | DDSCAPS2_CUBEMAP_POSITIVEY | DDSCAPS2_CUBEMAP_NEGATIVEY | DDSCAPS2_CUBEMAP_POSITIVEZ | DDSCAPS2_CUBEMAP_NEGATIVEZ;
  }

  bool dxt_format = false;
  if (pixel_format_helpers::is_dxt(m_format)) {
    dxt_format = true;

    desc.ddpfPixelFormat.dwFlags |= DDPF_FOURCC;

    switch (m_format) {
      case PIXEL_FMT_ETC1: {
        desc.ddpfPixelFormat.dwFourCC = (uint32)PIXEL_FMT_ETC1;
        desc.ddpfPixelFormat.dwRGBBitCount = 0;
        break;
      }
      case PIXEL_FMT_ETC2: {
        desc.ddpfPixelFormat.dwFourCC = (uint32)PIXEL_FMT_ETC2;
        desc.ddpfPixelFormat.dwRGBBitCount = 0;
        break;
      }
      case PIXEL_FMT_ETC2A: {
        desc.ddpfPixelFormat.dwFourCC = (uint32)PIXEL_FMT_ETC2A;
        desc.ddpfPixelFormat.dwRGBBitCount = 0;
        break;
      }
      case PIXEL_FMT_ETC1S: {
        desc.ddpfPixelFormat.dwFourCC = (uint32)PIXEL_FMT_ETC1S;
        desc.ddpfPixelFormat.dwRGBBitCount = 0;
        break;
      }
      case PIXEL_FMT_ETC2AS: {
        desc.ddpfPixelFormat.dwFourCC = (uint32)PIXEL_FMT_ETC2AS;
        desc.ddpfPixelFormat.dwRGBBitCount = 0;
        break;
      }
      case PIXEL_FMT_DXN: {
        desc.ddpfPixelFormat.dwFourCC = (uint32)PIXEL_FMT_3DC;
        desc.ddpfPixelFormat.dwRGBBitCount = PIXEL_FMT_DXN;
        break;
      }
      case PIXEL_FMT_DXT1A: {
        desc.ddpfPixelFormat.dwFourCC = (uint32)PIXEL_FMT_DXT1;
        desc.ddpfPixelFormat.dwRGBBitCount = 0;
        break;
      }
      case PIXEL_FMT_DXT5_CCxY: {
        desc.ddpfPixelFormat.dwFourCC = (uint32)PIXEL_FMT_DXT5;
        desc.ddpfPixelFormat.dwRGBBitCount = (uint32)PIXEL_FMT_DXT5_CCxY;
        break;
      }
      case PIXEL_FMT_DXT5_xGxR: {
        desc.ddpfPixelFormat.dwFourCC = (uint32)PIXEL_FMT_DXT5;
        desc.ddpfPixelFormat.dwRGBBitCount = (uint32)PIXEL_FMT_DXT5_xGxR;
        break;
      }
      case PIXEL_FMT_DXT5_xGBR: {
        desc.ddpfPixelFormat.dwFourCC = (uint32)PIXEL_FMT_DXT5;
        desc.ddpfPixelFormat.dwRGBBitCount = (uint32)PIXEL_FMT_DXT5_xGBR;
        break;
      }
      case PIXEL_FMT_DXT5_AGBR: {
        desc.ddpfPixelFormat.dwFourCC = (uint32)PIXEL_FMT_DXT5;
        desc.ddpfPixelFormat.dwRGBBitCount = (uint32)PIXEL_FMT_DXT5_AGBR;
        break;
      }
      default: {
        desc.ddpfPixelFormat.dwFourCC = (uint32)m_format;
        desc.ddpfPixelFormat.dwRGBBitCount = 0;
        break;
      }
    }

    uint bits_per_pixel = pixel_format_helpers::get_bpp(m_format);
    desc.lPitch = (((desc.dwWidth + 3) & ~3) * ((desc.dwHeight + 3) & ~3) * bits_per_pixel) >> 3;
    desc.dwFlags |= DDSD_LINEARSIZE;
  } else {
    switch (m_format) {
      case PIXEL_FMT_A8R8G8B8: {
        desc.ddpfPixelFormat.dwFlags |= (DDPF_RGB | DDPF_ALPHAPIXELS);
        desc.ddpfPixelFormat.dwRGBBitCount = 32;
        desc.ddpfPixelFormat.dwRBitMask = 0xFF0000;
        desc.ddpfPixelFormat.dwGBitMask = 0x00FF00;
        desc.ddpfPixelFormat.dwBBitMask = 0x0000FF;
        desc.ddpfPixelFormat.dwRGBAlphaBitMask = 0xFF000000;
        break;
      }
      case PIXEL_FMT_R8G8B8: {
        desc.ddpfPixelFormat.dwFlags |= DDPF_RGB;
        desc.ddpfPixelFormat.dwRGBBitCount = 24;
        desc.ddpfPixelFormat.dwRBitMask = 0xFF0000;
        desc.ddpfPixelFormat.dwGBitMask = 0x00FF00;
        desc.ddpfPixelFormat.dwBBitMask = 0x0000FF;
        break;
      }
      case PIXEL_FMT_A8: {
        desc.ddpfPixelFormat.dwFlags |= DDPF_ALPHA;
        desc.ddpfPixelFormat.dwRGBBitCount = 8;
        desc.ddpfPixelFormat.dwRGBAlphaBitMask = 0xFF;
        break;
      }
      case PIXEL_FMT_L8: {
        desc.ddpfPixelFormat.dwFlags |= DDPF_LUMINANCE;
        desc.ddpfPixelFormat.dwRGBBitCount = 8;
        desc.ddpfPixelFormat.dwRBitMask = 0xFF;
        break;
      }
      case PIXEL_FMT_A8L8: {
        desc.ddpfPixelFormat.dwFlags |= DDPF_ALPHAPIXELS | DDPF_LUMINANCE;
        desc.ddpfPixelFormat.dwRGBBitCount = 16;
        desc.ddpfPixelFormat.dwRBitMask = 0xFF;
        desc.ddpfPixelFormat.dwRGBAlphaBitMask = 0xFF00;
        break;
      }
      default: {
        CRNLIB_ASSERT(false);
        return false;
      }
    }

    uint bits_per_pixel = desc.ddpfPixelFormat.dwRGBBitCount;
    desc.lPitch = (desc.dwWidth * bits_per_pixel) >> 3;
    desc.dwFlags |= DDSD_LINEARSIZE;
  }

  if (!c_crnlib_little_endian_platform)
    utils::endian_switch_dwords(reinterpret_cast<uint32*>(&desc), sizeof(desc) / sizeof(uint32));

  if (!serializer.write(&desc, sizeof(desc)))
    return false;

  if (!c_crnlib_little_endian_platform)
    utils::endian_switch_dwords(reinterpret_cast<uint32*>(&desc), sizeof(desc) / sizeof(uint32));

  crnlib::vector<uint8> write_buf;

  const bool can_unflip_packed_texture = can_unflip_without_unpacking();
  if ((is_packed()) && (is_flipped()) && (!can_unflip_without_unpacking())) {
    console::warning("mipmapped_texture::write_dds: One or more faces/miplevels cannot be unflipped without unpacking. Writing flipped .DDS texture.");
  }

  for (uint face = 0; face < get_num_faces(); face++) {
    for (uint level = 0; level < get_num_levels(); level++) {
      const mip_level* pLevel = get_level(face, level);

      if (dxt_format) {
#if !defined(NDEBUG)
        const uint width = pLevel->get_width();
        const uint height = pLevel->get_height();
#endif

        CRNLIB_ASSERT(width == math::maximum<uint>(1, m_width >> level));
        CRNLIB_ASSERT(height == math::maximum<uint>(1, m_height >> level));

        const dxt_image* p = pLevel->get_dxt_image();
        dxt_image tmp;
        if ((can_unflip_packed_texture) && (pLevel->get_orientation_flags() & (cOrientationFlagXFlipped | cOrientationFlagYFlipped))) {
          tmp = *p;
          if (pLevel->get_orientation_flags() & cOrientationFlagXFlipped) {
            if (!tmp.flip_x())
              console::warning("mipmapped_texture::write_dds: Unable to unflip compressed texture on X axis");
          }

          if (pLevel->get_orientation_flags() & cOrientationFlagYFlipped) {
            if (!tmp.flip_y())
              console::warning("mipmapped_texture::write_dds: Unable to unflip compressed texture on Y axis");
          }
          p = &tmp;
        }

#if !defined(NDEBUG)
        const uint num_blocks_x = (width + 3) >> 2;
        const uint num_blocks_y = (height + 3) >> 2;
#endif

        CRNLIB_ASSERT(num_blocks_x * num_blocks_y * p->get_elements_per_block() == p->get_total_elements());

        const uint size_in_bytes = p->get_total_elements() * sizeof(dxt_image::element);
        if (size_in_bytes > write_buf.size())
          write_buf.resize(size_in_bytes);

        memcpy(&write_buf[0], p->get_element_ptr(), size_in_bytes);

        // DXT data is always little endian in memory, just like the DDS format.
        // (Except for ETC1, which contains big endian 64-bit QWORD's).
        //if (!c_crnlib_little_endian_platform)
        //   utils::endian_switch_words(reinterpret_cast<WORD*>(&write_buf[0]), size_in_bytes / sizeof(WORD));

        if (!serializer.write(&write_buf[0], size_in_bytes))
          return false;
      } else {
        const uint width = pLevel->get_width();
        const uint height = pLevel->get_height();

        const image_u8* p = pLevel->get_image();
        image_u8 tmp;
        if (pLevel->get_orientation_flags() & (cOrientationFlagXFlipped | cOrientationFlagYFlipped)) {
          p = pLevel->get_unpacked_image(tmp, cUnpackFlagUnflip);
        }

        const uint bits_per_pixel = desc.ddpfPixelFormat.dwRGBBitCount;
        const uint bytes_per_pixel = bits_per_pixel >> 3;

        const uint pitch = width * bytes_per_pixel;
        if (pitch > write_buf.size())
          write_buf.resize(pitch);

        for (uint y = 0; y < height; y++) {
          const color_quad_u8* pSrc = p->get_scanline(y);
          const color_quad_u8* pEnd = pSrc + width;

          uint8* pDst = &write_buf[0];

          do {
            const color_quad_u8& c = *pSrc;

            uint x = 0;
            switch (m_format) {
              case PIXEL_FMT_A8R8G8B8: {
                x = (c.a << 24) | (c.r << 16) | (c.g << 8) | c.b;
                break;
              }
              case PIXEL_FMT_R8G8B8: {
                x = (c.r << 16) | (c.g << 8) | c.b;
                break;
              }
              case PIXEL_FMT_A8: {
                x = c.a;
                break;
              }
              case PIXEL_FMT_A8L8: {
                x = (c.a << 8) | c.get_luma();
                break;
              }
              case PIXEL_FMT_L8: {
                x = c.get_luma();
                break;
              }
              default:
                break;
            }

            pDst[0] = static_cast<uint8>(x);
            if (bytes_per_pixel > 1) {
              pDst[1] = static_cast<uint8>(x >> 8);

              if (bytes_per_pixel > 2) {
                pDst[2] = static_cast<uint8>(x >> 16);

                if (bytes_per_pixel > 3)
                  pDst[3] = static_cast<uint8>(x >> 24);
              }
            }

            pSrc++;
            pDst += bytes_per_pixel;

          } while (pSrc != pEnd);

          if (!serializer.write(&write_buf[0], pitch))
            return false;
        }
      }
    }
  }

  clear_last_error();

  return true;
}

bool mipmapped_texture::read_ktx(data_stream_serializer& serializer) {
  clear();

  set_last_error("Unable to read KTX file");

  ktx_texture kt;
  if (!kt.read_from_stream(serializer))
    return false;

  if ((kt.get_depth() > 1) || (kt.get_array_size() > 1)) {
    set_last_error("read_ktx: Depth and array textures are not supported");
    return false;
  }

  // Must be 1D, 2D, or a cubemap, with or without mipmaps.
  m_width = kt.get_width();
  m_height = kt.get_height();

  uint num_mip_levels = kt.get_num_mips();
  uint num_faces = kt.get_num_faces();

  uint32 crnlib_fourcc = 0;
  dynamic_string crnlib_fourcc_str;
  if (kt.get_key_value_as_string("CRNLIB_FOURCC", crnlib_fourcc_str)) {
    if (crnlib_fourcc_str.get_len() == 4) {
      for (int i = 3; i >= 0; i--)
        crnlib_fourcc = (crnlib_fourcc << 8) | crnlib_fourcc_str[i];
    }
  }

  const bool is_compressed_texture = kt.is_compressed();
  dxt_format dxt_fmt = cDXTInvalid;

  pixel_packer unpacker;
  if (is_compressed_texture) {
    switch (kt.get_ogl_internal_fmt()) {
      case KTX_ETC1_RGB8_OES:
        dxt_fmt = cETC1;
        break;
      case KTX_COMPRESSED_RGB8_ETC2:
        dxt_fmt = cETC2;
        break;
      case KTX_COMPRESSED_RGBA8_ETC2_EAC:
        dxt_fmt = cETC2A;
        break;
      case KTX_RGB_S3TC:
      case KTX_RGB4_S3TC:
      case KTX_COMPRESSED_RGB_S3TC_DXT1_EXT:
      case KTX_COMPRESSED_SRGB_S3TC_DXT1_EXT:
        dxt_fmt = cDXT1;
        break;
      case KTX_COMPRESSED_RGBA_S3TC_DXT1_EXT:
      case KTX_COMPRESSED_SRGB_ALPHA_S3TC_DXT1_EXT:
        dxt_fmt = cDXT1A;
        break;
      case KTX_RGBA_S3TC:
      case KTX_RGBA4_S3TC:
      case KTX_COMPRESSED_RGBA_S3TC_DXT3_EXT:
      case KTX_COMPRESSED_SRGB_ALPHA_S3TC_DXT3_EXT:
        dxt_fmt = cDXT3;
        break;
      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:
        dxt_fmt = cDXT5;
        break;
      case KTX_COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT:
        dxt_fmt = cDXN_YX;
        if (crnlib_fourcc == PIXEL_FMT_DXN) {
          dxt_fmt = cDXN_XY;
        }
        break;
      case KTX_COMPRESSED_LUMINANCE_LATC1_EXT:
        dxt_fmt = cDXT5A;
        break;
      default:
        set_last_error("Unsupported KTX internal format");
        return false;
    }

    m_format = pixel_format_helpers::from_dxt_format(dxt_fmt);
    if (m_format == PIXEL_FMT_INVALID) {
      set_last_error("Unsupported KTX internal compressed format");
      return false;
    }

    if (crnlib_fourcc != 0) {
      switch (crnlib_fourcc) {
        case PIXEL_FMT_DXT5_CCxY:
        case PIXEL_FMT_DXT5_xGxR:
        case PIXEL_FMT_DXT5_xGBR:
        case PIXEL_FMT_DXT5_AGBR: {
          if (dxt_fmt == cDXT5) {
            m_format = static_cast<pixel_format>(crnlib_fourcc);
          }
          break;
        }
      }
    }
  } else {
    m_format = PIXEL_FMT_A8R8G8B8;
    const uint type_size = get_ogl_type_size(kt.get_ogl_type());
    const uint type_bits = type_size * 8;

    // Normal component order:   1,2,3,4 (*last* component packed into LSB of output type)
    // Reversed component order: 4,3,2,1 (*first* component packed into LSB of output type)

    if (is_packed_pixel_ogl_type(kt.get_ogl_type())) {
      switch (kt.get_ogl_type()) {
        // 24bpp packed formats
        case KTX_UNSIGNED_BYTE_3_3_2:
          unpacker.init("B2G3R3");
          m_format = PIXEL_FMT_R8G8B8;
          break;
        case KTX_UNSIGNED_BYTE_2_3_3_REV:
          unpacker.init("R3G3B2");
          m_format = PIXEL_FMT_R8G8B8;
          break;
        case KTX_UNSIGNED_SHORT_5_6_5:
          unpacker.init("B5G6R5");
          m_format = PIXEL_FMT_R8G8B8;
          break;
        case KTX_UNSIGNED_SHORT_5_6_5_REV:
          unpacker.init("R5G6B5");
          m_format = PIXEL_FMT_R8G8B8;
          break;
        // 32bpp packed formats
        case KTX_UNSIGNED_SHORT_4_4_4_4:
          unpacker.init("A4B4G4R4");
          break;
        case KTX_UNSIGNED_SHORT_4_4_4_4_REV:
          unpacker.init("R4G4B4A4");
          break;
        case KTX_UNSIGNED_SHORT_5_5_5_1:
          unpacker.init("A1B5G5R5");
          break;
        case KTX_UNSIGNED_SHORT_1_5_5_5_REV:
          unpacker.init("R5G5B5A1");
          break;
        case KTX_UNSIGNED_INT_8_8_8_8:
          unpacker.init("A8B8G8R8");
          break;
        case KTX_UNSIGNED_INT_8_8_8_8_REV:
          unpacker.init("R8G8B8A8");
          break;
        case KTX_UNSIGNED_INT_10_10_10_2:
          unpacker.init("A2B10G10R10");
          break;
        case KTX_UNSIGNED_INT_2_10_10_10_REV:
          unpacker.init("R10G10B10A2");
          break;
        case KTX_UNSIGNED_INT_5_9_9_9_REV:
          unpacker.init("R9G9B9A5");
          break;
        default:
          set_last_error("Unsupported KTX packed pixel type");
          return false;
      }

      unpacker.set_pixel_stride(get_ogl_type_size(kt.get_ogl_type()));
    } else {
      switch (kt.get_ogl_fmt()) {
        case 1:
        case KTX_RED:
        case KTX_RED_INTEGER:
        case KTX_R8:
        case KTX_R8UI: {
          unpacker.init("R", -1, type_bits);
          m_format = PIXEL_FMT_R8G8B8;
          break;
        }
        case KTX_GREEN:
        case KTX_GREEN_INTEGER: {
          unpacker.init("G", -1, type_bits);
          m_format = PIXEL_FMT_R8G8B8;
          break;
        }
        case KTX_BLUE:
        case KTX_BLUE_INTEGER: {
          unpacker.init("B", -1, type_bits);
          m_format = PIXEL_FMT_R8G8B8;
          break;
        }
        case KTX_ALPHA: {
          unpacker.init("A", -1, type_bits);
          m_format = PIXEL_FMT_A8;
          break;
        }
        case KTX_LUMINANCE: {
          unpacker.init("Y", -1, type_bits);
          m_format = PIXEL_FMT_L8;
          break;
        }
        case 2:
        case KTX_RG:
        case KTX_RG8:
        case KTX_RG_INTEGER: {
          unpacker.init("RG", -1, type_bits);
          m_format = PIXEL_FMT_A8L8;
          break;
        }
        case KTX_LUMINANCE_ALPHA: {
          unpacker.init("YA", -1, type_bits);
          m_format = PIXEL_FMT_A8L8;
          break;
        }
        case 3:
        case KTX_SRGB:
        case KTX_RGB:
        case KTX_RGB_INTEGER:
        case KTX_RGB8:
        case KTX_SRGB8: {
          unpacker.init("RGB", -1, type_bits);
          m_format = PIXEL_FMT_R8G8B8;
          break;
        }
        case KTX_BGR:
        case KTX_BGR_INTEGER: {
          unpacker.init("BGR", -1, type_bits);
          m_format = PIXEL_FMT_R8G8B8;
          break;
        }
        case 4:
        case KTX_RGBA_INTEGER:
        case KTX_RGBA:
        case KTX_SRGB_ALPHA:
        case KTX_SRGB8_ALPHA8:
        case KTX_RGBA8: {
          unpacker.init("RGBA", -1, type_bits);
          break;
        }
        case KTX_BGRA:
        case KTX_BGRA_INTEGER: {
          unpacker.init("BGRA", -1, type_bits);
          break;
        }
        default:
          set_last_error("Unsupported KTX pixel format");
          return false;
      }

      unpacker.set_pixel_stride(unpacker.get_num_comps() * get_ogl_type_size(kt.get_ogl_type()));
    }

    CRNLIB_ASSERT(unpacker.is_valid());
  }

  m_comp_flags = pixel_format_helpers::get_component_flags(m_format);

  m_faces.resize(num_faces);

  bool x_flipped = false;
  bool y_flipped = true;

  dynamic_string orient;
  if ((kt.get_key_value_as_string("KTXorientation", orient)) && (orient.get_len() >= 7)) {
    //  0123456
    // "S=r,T=d"
    if ((orient[0] == 'S') && (orient[1] == '=') && (orient[3] == ',') &&
        (orient[4] == 'T') && (orient[5] == '=')) {
      if (tolower(orient[2]) == 'l')
        x_flipped = true;
      else if (tolower(orient[2]) == 'r')
        x_flipped = false;

      if (tolower(orient[6]) == 'u')
        y_flipped = true;
      else if (tolower(orient[6]) == 'd')
        y_flipped = false;
    }
  }

  orientation_flags_t orient_flags = cDefaultOrientationFlags;
  if (x_flipped)
    orient_flags = static_cast<orientation_flags_t>(orient_flags | cOrientationFlagXFlipped);
  if (y_flipped)
    orient_flags = static_cast<orientation_flags_t>(orient_flags | cOrientationFlagYFlipped);

  bool dxt1_alpha = false;

  for (uint face_index = 0; face_index < num_faces; face_index++) {
    m_faces[face_index].resize(num_mip_levels);

    for (uint level_index = 0; level_index < num_mip_levels; level_index++) {
      const uint width = math::maximum<uint>(m_width >> level_index, 1U);
      const uint height = math::maximum<uint>(m_height >> level_index, 1U);

      mip_level* pMip = crnlib_new<mip_level>();
      m_faces[face_index][level_index] = pMip;

      const crnlib::vector<uint8>& image_data = kt.get_image_data(level_index, 0, face_index, 0);

      if (is_compressed_texture) {
        const uint bytes_per_block = pixel_format_helpers::get_dxt_bytes_per_block(m_format);

        const uint num_blocks_x = (width + 3) >> 2;
        const uint num_blocks_y = (height + 3) >> 2;

        const uint level_pitch = num_blocks_x * num_blocks_y * bytes_per_block;
        if (image_data.size() != level_pitch)
          return false;

        dxt_image* pDXTImage = crnlib_new<dxt_image>();
        if (!pDXTImage->init(dxt_fmt, width, height, false)) {
          crnlib_delete(pDXTImage);

          CRNLIB_ASSERT(0);
          return false;
        }

        CRNLIB_ASSERT(pDXTImage->get_element_vec().size() * sizeof(dxt_image::element) == level_pitch);

        memcpy(&pDXTImage->get_element_vec()[0], image_data.get_ptr(), image_data.size());

        if ((m_format == PIXEL_FMT_DXT1) && (!dxt1_alpha))
          dxt1_alpha = pDXTImage->has_alpha();

        pMip->assign(pDXTImage, m_format, orient_flags);
      } else {
        if (image_data.size() != (width * height * unpacker.get_pixel_stride()))
          return false;

        image_u8* pImage = crnlib_new<image_u8>(width, height);

        pImage->set_comp_flags(m_comp_flags);

        const uint8* pSrc = image_data.get_ptr();

        color_quad_u8 q(0, 0, 0, 255);

        for (uint y = 0; y < height; y++) {
          for (uint x = 0; x < width; x++) {
            color_quad_u8 c;
            pSrc = static_cast<const uint8*>(unpacker.unpack(pSrc, c));
            pImage->set_pixel_unclipped(x, y, c);
          }
        }

        pMip->assign(pImage, m_format, orient_flags);

        CRNLIB_ASSERT(pMip->get_comp_flags() == m_comp_flags);
      }
    }
  }

  clear_last_error();

  if (dxt1_alpha)
    change_dxt1_to_dxt1a();

  return true;
}

bool mipmapped_texture::write_ktx(data_stream_serializer& serializer) const {
  if (!m_width) {
    set_last_error("Nothing to write");
    return false;
  }

  set_last_error("write_ktx() failed");

  uint32 ogl_internal_fmt = 0, ogl_fmt = 0, ogl_type = 0;

  pixel_packer packer;

  if (is_packed()) {
    switch (get_format()) {
      case PIXEL_FMT_DXT1: {
        ogl_internal_fmt = KTX_COMPRESSED_RGB_S3TC_DXT1_EXT;
        break;
      }
      case PIXEL_FMT_DXT1A: {
        ogl_internal_fmt = KTX_COMPRESSED_RGBA_S3TC_DXT1_EXT;
        break;
      }
      case PIXEL_FMT_DXT2:
      case PIXEL_FMT_DXT3: {
        ogl_internal_fmt = KTX_COMPRESSED_RGBA_S3TC_DXT3_EXT;
        break;
      }
      case PIXEL_FMT_DXT4:
      case PIXEL_FMT_DXT5:
      case PIXEL_FMT_DXT5_CCxY:
      case PIXEL_FMT_DXT5_xGxR:
      case PIXEL_FMT_DXT5_xGBR:
      case PIXEL_FMT_DXT5_AGBR: {
        ogl_internal_fmt = KTX_COMPRESSED_RGBA_S3TC_DXT5_EXT;
        break;
      }
      case PIXEL_FMT_3DC:
      case PIXEL_FMT_DXN: {
        ogl_internal_fmt = KTX_COMPRESSED_SIGNED_RED_GREEN_RGTC2_EXT;
        break;
      }
      case PIXEL_FMT_DXT5A: {
        ogl_internal_fmt = KTX_COMPRESSED_LUMINANCE_LATC1_EXT;
        break;
      }
      case PIXEL_FMT_ETC1:
      case PIXEL_FMT_ETC1S: {
        ogl_internal_fmt = KTX_ETC1_RGB8_OES;
        break;
      }
      case PIXEL_FMT_ETC2: {
        ogl_internal_fmt = KTX_COMPRESSED_RGB8_ETC2;
        break;
      }
      case PIXEL_FMT_ETC2A:
      case PIXEL_FMT_ETC2AS: {
        ogl_internal_fmt = KTX_COMPRESSED_RGBA8_ETC2_EAC;
        break;
      }
      default: {
        CRNLIB_ASSERT(0);
        return false;
      }
    }
  } else {
    ogl_type = KTX_UNSIGNED_BYTE;

    switch (get_format()) {
      case PIXEL_FMT_R8G8B8:
        ogl_internal_fmt = KTX_RGB8;
        ogl_fmt = KTX_RGB;
        packer.init("R8G8B8");
        break;
      case PIXEL_FMT_L8:
        ogl_internal_fmt = KTX_LUMINANCE8;
        ogl_fmt = KTX_LUMINANCE;
        packer.init("G8");
        break;
      case PIXEL_FMT_A8:
        ogl_internal_fmt = KTX_ALPHA8;
        ogl_fmt = KTX_ALPHA;
        packer.init("A8");
        break;
      case PIXEL_FMT_A8L8:
        ogl_internal_fmt = KTX_LUMINANCE8_ALPHA8;
        ogl_fmt = KTX_LUMINANCE_ALPHA;
        packer.init("Y8A8");
        break;
      case PIXEL_FMT_A8R8G8B8:
        ogl_internal_fmt = KTX_RGBA8;
        ogl_fmt = KTX_RGBA;
        packer.init("R8G8B8A8");
        break;
      default: {
        CRNLIB_ASSERT(0);
        return false;
      }
    }
  }

  ktx_texture kt;
  bool success;
  if (determine_texture_type(false) == cTextureTypeCubemap)
    success = kt.init_cubemap(get_width(), get_num_levels(), ogl_internal_fmt, ogl_fmt, ogl_type);
  else
    success = kt.init_2D(get_width(), get_height(), get_num_levels(), ogl_internal_fmt, ogl_fmt, ogl_type);
  if (!success)
    return false;

  dynamic_string fourcc_str(cVarArg, "%c%c%c%c", m_format & 0xFF, (m_format >> 8) & 0xFF, (m_format >> 16) & 0xFF, (m_format >> 24) & 0xFF);
  kt.add_key_value("CRNLIB_FOURCC", fourcc_str.get_ptr());

  const mip_level* pLevel0 = get_level(0, 0);
  dynamic_string ktx_orient_str(cVarArg, "S=%c,T=%c", (pLevel0->get_orientation_flags() & cOrientationFlagXFlipped) ? 'l' : 'r', (pLevel0->get_orientation_flags() & cOrientationFlagYFlipped) ? 'u' : 'd');
  kt.add_key_value("KTXorientation", ktx_orient_str.get_ptr());

  for (uint face_index = 0; face_index < get_num_faces(); face_index++) {
    for (uint level_index = 0; level_index < get_num_levels(); level_index++) {
      const mip_level* pLevel = get_level(face_index, level_index);

      const uint mip_width = pLevel->get_width();
      const uint mip_height = pLevel->get_height();

      if (is_packed()) {
        const dxt_image* p = pLevel->get_dxt_image();

        kt.add_image(face_index, level_index, p->get_element_ptr(), p->get_size_in_bytes());
      } else {
        const image_u8* p = pLevel->get_image();

        crnlib::vector<uint8> tmp(mip_width * mip_height * packer.get_pixel_stride());

        uint8* pDst = tmp.get_ptr();
        for (uint y = 0; y < mip_height; y++)
          for (uint x = 0; x < mip_width; x++)
            pDst = (uint8*)packer.pack(p->get_unclamped(x, y), pDst);

        kt.add_image(face_index, level_index, tmp.get_ptr(), tmp.size_in_bytes());
      }
    }
  }

  if (!kt.write_to_stream(serializer))
    return false;

  clear_last_error();
  return true;
}

void mipmapped_texture::assign(face_vec& faces) {
  CRNLIB_ASSERT(!faces.empty());
  if (faces.empty())
    return;

  free_all_mips();

#ifdef CRNLIB_BUILD_DEBUG
  for (uint i = 1; i < faces.size(); i++)
    CRNLIB_ASSERT(faces[i].size() == faces[0].size());
#endif

  mip_level* p = faces[0][0];
  m_width = p->get_width();
  m_height = p->get_height();
  m_comp_flags = p->get_comp_flags();
  m_format = p->get_format();

  m_faces.swap(faces);

  CRNLIB_ASSERT(check());
}

void mipmapped_texture::assign(mip_level* pLevel) {
  face_vec faces(1, mip_ptr_vec(1, pLevel));
  assign(faces);
}

void mipmapped_texture::assign(image_u8* p, pixel_format fmt, orientation_flags_t orient_flags) {
  mip_level* pLevel = crnlib_new<mip_level>();
  pLevel->assign(p, fmt, orient_flags);
  assign(pLevel);
}

void mipmapped_texture::assign(dxt_image* p, pixel_format fmt, orientation_flags_t orient_flags) {
  mip_level* pLevel = crnlib_new<mip_level>();
  pLevel->assign(p, fmt, orient_flags);
  assign(pLevel);
}

void mipmapped_texture::set(texture_file_types::format source_file_type, const mipmapped_texture& mipmapped_texture) {
  clear();

  *this = mipmapped_texture;
  m_source_file_type = source_file_type;
}

image_u8* mipmapped_texture::get_level_image(uint face, uint level, image_u8& img, uint unpack_flags) const {
  if (!is_valid())
    return NULL;

  const mip_level* pLevel = get_level(face, level);

  return pLevel->get_unpacked_image(img, unpack_flags);
}

void mipmapped_texture::swap(mipmapped_texture& img) {
  utils::swap(m_width, img.m_width);
  utils::swap(m_height, img.m_height);
  utils::swap(m_comp_flags, img.m_comp_flags);
  utils::swap(m_format, img.m_format);
  m_faces.swap(img.m_faces);
  m_last_error.swap(img.m_last_error);
  utils::swap(m_source_file_type, img.m_source_file_type);

  CRNLIB_ASSERT(check());
}

texture_type mipmapped_texture::determine_texture_type(bool no_normal_detection) const {
  if (!is_valid())
    return cTextureTypeUnknown;

  if (get_num_faces() == 6)
    return cTextureTypeCubemap;
  else if (is_vertical_cross())
    return cTextureTypeVerticalCrossCubemap;
  else if (!no_normal_detection && is_normal_map())
    return cTextureTypeNormalMap;

  return cTextureTypeRegularMap;
}

void mipmapped_texture::discard_mips() {
  for (uint f = 0; f < m_faces.size(); f++) {
    if (m_faces[f].size() > 1) {
      for (uint l = 1; l < m_faces[f].size(); l++)
        crnlib_delete(m_faces[f][l]);

      m_faces[f].resize(1);
    }
  }

  CRNLIB_ASSERT(check());
}

void mipmapped_texture::init(uint width, uint height, uint levels, uint faces, pixel_format fmt, const char* pName, orientation_flags_t orient_flags) {
  clear();

  CRNLIB_ASSERT((width > 0) && (height > 0) && (levels > 0));
  CRNLIB_ASSERT((faces == 1) || (faces == 6));

  m_width = width;
  m_height = height;
  m_comp_flags = pixel_format_helpers::get_component_flags(fmt);
  m_format = fmt;
  if (pName)
    m_name.set(pName);

  m_faces.resize(faces);
  for (uint f = 0; f < faces; f++) {
    m_faces[f].resize(levels);
    for (uint l = 0; l < levels; l++) {
      m_faces[f][l] = crnlib_new<mip_level>();

      const uint mip_width = math::maximum(1U, width >> l);
      const uint mip_height = math::maximum(1U, height >> l);
      if (pixel_format_helpers::is_dxt(fmt)) {
        dxt_image* p = crnlib_new<dxt_image>();
        p->init(pixel_format_helpers::get_dxt_format(fmt), mip_width, mip_height, true);
        m_faces[f][l]->assign(p, m_format, orient_flags);
      } else {
        image_u8* p = crnlib_new<image_u8>(mip_width, mip_height);
        p->set_comp_flags(m_comp_flags);
        m_faces[f][l]->assign(p, m_format, orient_flags);
      }
    }
  }

  CRNLIB_ASSERT(check());
}

void mipmapped_texture::discard_mipmaps() {
  if (!is_valid())
    return;

  discard_mips();
}

bool mipmapped_texture::convert(pixel_format fmt, bool cook, const dxt_image::pack_params& p) {
  if (!is_valid())
    return false;

  if (fmt == get_format())
    return true;

  uint total_pixels = 0;
  for (uint f = 0; f < m_faces.size(); f++)
    for (uint l = 0; l < m_faces[f].size(); l++)
      total_pixels += m_faces[f][l]->get_total_pixels();

  uint progress_start = p.m_progress_start;

  for (uint f = 0; f < m_faces.size(); f++) {
    for (uint l = 0; l < m_faces[f].size(); l++) {
      const uint num_pixels = m_faces[f][l]->get_total_pixels();

      uint progress_range = (num_pixels * p.m_progress_range) / total_pixels;

      dxt_image::pack_params tmp_params(p);
      tmp_params.m_progress_start = math::clamp<uint>(progress_start, 0, p.m_progress_range);
      tmp_params.m_progress_range = math::clamp<uint>(progress_range, 0, p.m_progress_range - tmp_params.m_progress_start);

      progress_start += tmp_params.m_progress_range;

      if (!m_faces[f][l]->convert(fmt, cook, tmp_params)) {
        clear();
        return false;
      }
    }
  }

  m_format = get_level(0, 0)->get_format();
  m_comp_flags = get_level(0, 0)->get_comp_flags();

  CRNLIB_ASSERT(check());

  if (p.m_pProgress_callback) {
    if (!p.m_pProgress_callback(p.m_progress_start + p.m_progress_range, p.m_pProgress_callback_user_data_ptr))
      return false;
  }

  return true;
}

bool mipmapped_texture::convert(pixel_format fmt, const dxt_image::pack_params& p) {
  return convert(fmt, true, p);
}

bool mipmapped_texture::convert(pixel_format fmt, bool cook, const dxt_image::pack_params& p, int qdxt_quality, bool hierarchical) {
  if ((!pixel_format_helpers::is_dxt(fmt)) || (fmt == PIXEL_FMT_DXT3) || (fmt == PIXEL_FMT_ETC1) || (fmt == PIXEL_FMT_ETC2) || (fmt == PIXEL_FMT_ETC2A) || (fmt == PIXEL_FMT_ETC1S) || (fmt == PIXEL_FMT_ETC2AS)) {
    // QDXT doesn't support DXT3 or ETCn yet.
    return convert(fmt, cook, p);
  }

  mipmapped_texture src_tex(*this);

  if (src_tex.is_packed())
    src_tex.unpack_from_dxt(true);

  if (cook) {
    mipmapped_texture cooked_tex(src_tex);

    for (uint f = 0; f < m_faces.size(); f++)
      for (uint l = 0; l < m_faces[f].size(); l++)
        src_tex.m_faces[f][l]->cook_image(*cooked_tex.m_faces[f][l]->get_image());

    src_tex.swap(cooked_tex);
  }

  qdxt1_params q1_params;
  q1_params.init(p, qdxt_quality, hierarchical);

  qdxt5_params q5_params;
  q5_params.init(p, qdxt_quality, hierarchical);

  if (pixel_format_helpers::is_pixel_format_non_srgb(fmt) || (m_comp_flags & pixel_format_helpers::cCompFlagNormalMap) || (m_comp_flags & pixel_format_helpers::cCompFlagLumaChroma)) {
    // Disable perceptual colorspace metrics when packing to swizzled or non-RGB pixel formats.
    q1_params.m_perceptual = false;
  }

  task_pool tp;
  if (!tp.init(p.m_num_helper_threads))
    return false;

  mipmapped_texture packed_tex;

  qdxt_state state(tp);
  if (!src_tex.qdxt_pack_init(state, packed_tex, q1_params, q5_params, fmt, false))
    return false;

  if (!src_tex.qdxt_pack(state, packed_tex, q1_params, q5_params))
    return false;

  swap(packed_tex);

  return true;
}

bool mipmapped_texture::is_packed() const {
  CRNLIB_ASSERT(is_valid());
  if (!is_valid())
    return false;

  return get_level(0, 0)->is_packed();
}

bool mipmapped_texture::set_alpha_to_luma() {
  CRNLIB_ASSERT(is_valid());
  if (!is_valid())
    return false;

  if (is_packed())
    unpack_from_dxt(true);

  for (uint f = 0; f < m_faces.size(); f++)
    for (uint l = 0; l < get_num_levels(); l++)
      get_level(f, l)->set_alpha_to_luma();

  m_format = get_level(0, 0)->get_format();
  m_comp_flags = get_level(0, 0)->get_comp_flags();

  CRNLIB_ASSERT(check());

  return true;
}

bool mipmapped_texture::convert(image_utils::conversion_type conv_type) {
  CRNLIB_ASSERT(is_valid());
  if (!is_valid())
    return false;

  if (is_packed())
    unpack_from_dxt(true);

  for (uint f = 0; f < m_faces.size(); f++)
    for (uint l = 0; l < get_num_levels(); l++)
      get_level(f, l)->convert(conv_type);

  m_format = get_level(0, 0)->get_format();
  m_comp_flags = get_level(0, 0)->get_comp_flags();

  CRNLIB_ASSERT(check());

  return true;
}

bool mipmapped_texture::unpack_from_dxt(bool uncook) {
  CRNLIB_ASSERT(is_valid());
  if (!is_valid())
    return false;

  CRNLIB_ASSERT(pixel_format_helpers::is_dxt(m_format));
  if (!pixel_format_helpers::is_dxt(m_format))
    return false;

  for (uint f = 0; f < m_faces.size(); f++)
    for (uint l = 0; l < get_num_levels(); l++)
      if (!get_level(f, l)->unpack_from_dxt(uncook))
        return false;

  m_format = get_level(0, 0)->get_format();
  m_comp_flags = get_level(0, 0)->get_comp_flags();

  CRNLIB_ASSERT(check());

  return true;
}

bool mipmapped_texture::has_alpha() const {
  CRNLIB_ASSERT(is_valid());
  if (!is_valid())
    return false;

  if (pixel_format_helpers::has_alpha(m_format))
    return true;

  if ((m_format == PIXEL_FMT_DXT1) && (get_level(0, 0)->get_dxt_image())) {
    // Try scanning DXT1 mip levels to find blocks with transparent pixels.
    for (uint f = 0; f < get_num_faces(); f++)
      if (get_level(f, 0)->get_dxt_image()->has_alpha())
        return true;
  }

  return false;
}

bool mipmapped_texture::is_normal_map() const {
  CRNLIB_ASSERT(is_valid());
  if (!is_valid())
    return false;

  if (pixel_format_helpers::is_normal_map(get_format()))
    return true;

  const mip_level* pLevel = get_level(0, 0);

  if (pLevel->get_image())
    return image_utils::is_normal_map(*pLevel->get_image(), m_name.get_ptr());

  image_u8 tmp;
  pLevel->get_dxt_image()->unpack(tmp);
  return image_utils::is_normal_map(tmp, m_name.get_ptr());
}

bool mipmapped_texture::is_vertical_cross() const {
  CRNLIB_ASSERT(is_valid());
  if (!is_valid())
    return false;

  if (get_num_faces() > 1)
    return false;

  if (!((math::is_power_of_2(m_height)) && (!math::is_power_of_2(m_width)) && (m_height / 4U == m_width / 3U)))
    return false;

  return true;
}

bool mipmapped_texture::resize(uint new_width, uint new_height, const resample_params& params) {
  CRNLIB_ASSERT(is_valid());
  if (!is_valid())
    return false;

  CRNLIB_ASSERT((new_width >= 1) && (new_height >= 1));

  face_vec faces(get_num_faces());
  for (uint f = 0; f < faces.size(); f++) {
    faces[f].resize(1);
    faces[f][0] = crnlib_new<mip_level>();
  }

  for (uint f = 0; f < faces.size(); f++) {
    image_u8 tmp;
    image_u8* pImg = get_level(f, 0)->get_unpacked_image(tmp, cUnpackFlagUncook);

    image_u8* pMip = crnlib_new<image_u8>();

    image_utils::resample_params rparams;
    rparams.m_dst_width = new_width;
    rparams.m_dst_height = new_height;
    rparams.m_filter_scale = params.m_filter_scale;
    rparams.m_first_comp = 0;
    rparams.m_num_comps = pImg->is_component_valid(3) ? 4 : 3;
    rparams.m_srgb = params.m_srgb;
    rparams.m_wrapping = params.m_wrapping;
    rparams.m_pFilter = params.m_pFilter;
    rparams.m_multithreaded = params.m_multithreaded;

    if (!image_utils::resample(*pImg, *pMip, rparams)) {
      crnlib_delete(pMip);

      for (uint f = 0; f < faces.size(); f++)
        for (uint l = 0; l < faces[f].size(); l++)
          crnlib_delete(faces[f][l]);

      return false;
    }

    if (params.m_renormalize)
      image_utils::renorm_normal_map(*pMip);

    pMip->set_comp_flags(pImg->get_comp_flags());

    faces[f][0]->assign(pMip, PIXEL_FMT_INVALID, get_level(f, 0)->get_orientation_flags());
  }

  assign(faces);

  CRNLIB_ASSERT(check());

  return true;
}

bool mipmapped_texture::generate_mipmaps(const generate_mipmap_params& params, bool force) {
  CRNLIB_ASSERT(is_valid());
  if (!is_valid())
    return false;

  uint num_levels = 1;
  {
    uint width = get_width();
    uint height = get_height();
    while ((width > params.m_min_mip_size) || (height > params.m_min_mip_size)) {
      width >>= 1U;
      height >>= 1U;
      num_levels++;
    }
  }

  if ((params.m_max_mips > 0) && (num_levels > params.m_max_mips))
    num_levels = params.m_max_mips;

  if ((force) && (get_num_levels() > 1))
    discard_mipmaps();

  if (num_levels == get_num_levels())
    return true;

  face_vec faces(get_num_faces());
  for (uint f = 0; f < faces.size(); f++) {
    faces[f].resize(num_levels);
    for (uint l = 0; l < num_levels; l++)
      faces[f][l] = crnlib_new<mip_level>();
  }

  for (uint f = 0; f < faces.size(); f++) {
    image_u8 tmp;
    image_u8* pImg = get_level(f, 0)->get_unpacked_image(tmp, cUnpackFlagUncook);

    for (uint l = 0; l < num_levels; l++) {
      const uint mip_width = math::maximum<uint>(1U, get_width() >> l);
      const uint mip_height = math::maximum<uint>(1U, get_height() >> l);

      image_u8* pMip = crnlib_new<image_u8>();

      if (!l)
        *pMip = *pImg;
      else {
        image_utils::resample_params rparams;
        rparams.m_dst_width = mip_width;
        rparams.m_dst_height = mip_height;
        rparams.m_filter_scale = params.m_filter_scale;
        rparams.m_first_comp = 0;
        rparams.m_num_comps = pImg->is_component_valid(3) ? 4 : 3;
        rparams.m_srgb = params.m_srgb;
        rparams.m_wrapping = params.m_wrapping;
        rparams.m_pFilter = params.m_pFilter;
        rparams.m_multithreaded = params.m_multithreaded;

        if (!image_utils::resample(*pImg, *pMip, rparams)) {
          crnlib_delete(pMip);

          for (uint f = 0; f < faces.size(); f++)
            for (uint l = 0; l < faces[f].size(); l++)
              crnlib_delete(faces[f][l]);

          return false;
        }

        if (params.m_renormalize)
          image_utils::renorm_normal_map(*pMip);

        pMip->set_comp_flags(pImg->get_comp_flags());
      }

      faces[f][l]->assign(pMip, PIXEL_FMT_INVALID, get_level(f, 0)->get_orientation_flags());
    }
  }

  assign(faces);

  CRNLIB_ASSERT(check());

  return true;
}

bool mipmapped_texture::crop(uint x, uint y, uint width, uint height) {
  CRNLIB_ASSERT(is_valid());
  if (!is_valid())
    return false;
  if (get_num_faces() > 1)
    return false;

  if ((width < 1) || (height < 1))
    return false;

  image_u8 tmp;
  image_u8* pImg = get_level(0, 0)->get_unpacked_image(tmp, cUnpackFlagUncook | cUnpackFlagUnflip);

  image_u8* pMip = crnlib_new<image_u8>(width, height);

  if (!pImg->extract_block(pMip->get_ptr(), x, y, width, height))
    return false;

  face_vec faces(1);
  faces[0].resize(1);
  faces[0][0] = crnlib_new<mip_level>();

  pMip->set_comp_flags(pImg->get_comp_flags());

  faces[0][0]->assign(pMip);

  assign(faces);

  CRNLIB_ASSERT(check());

  return true;
}

bool mipmapped_texture::vertical_cross_to_cubemap() {
  if (!is_vertical_cross())
    return false;

  const uint face_width = get_height() / 4;

  bool alpha_is_valid = has_alpha();

  mipmapped_texture cubemap;

  pixel_format fmt = alpha_is_valid ? PIXEL_FMT_A8R8G8B8 : PIXEL_FMT_R8G8B8;

  cubemap.init(face_width, face_width, 1, 6, fmt, m_name.get_ptr(), cDefaultOrientationFlags);

  // +x -x +y -y +z -z
  //     0  1  2
  // 0     +y
  // 1  -x +z +x
  // 2     -y
  // 3     -z

  for (uint face_index = 0; face_index < 6; face_index++) {
    const mip_level* pSrc = get_level(0, 0);

    image_u8 tmp_img;
    image_u8* pSrc_image = pSrc->get_unpacked_image(tmp_img, cUnpackFlagUncook | cUnpackFlagUnflip);

    const mip_level* pDst = get_level(face_index, 0);
    image_u8* pDst_image = pDst->get_image();
    CRNLIB_ASSERT(pDst_image);

    const bool flipped = (face_index == 5);
    const uint x_ofs = g_vertical_cross_image_offsets[face_index][0] * face_width;
    const uint y_ofs = g_vertical_cross_image_offsets[face_index][1] * face_width;

    for (uint y = 0; y < face_width; y++) {
      for (uint x = 0; x < face_width; x++) {
        const color_quad_u8& c = (*pSrc_image)(x_ofs + x, y_ofs + y);

        if (!flipped)
          (*pDst_image)(x, y) = c;
        else
          (*pDst_image)(face_width - 1 - x, face_width - 1 - y) = c;
      }
    }
  }

  swap(cubemap);

  CRNLIB_ASSERT(check());

  return true;
}

bool mipmapped_texture::qdxt_pack_init(qdxt_state& state, mipmapped_texture& dst_tex, const qdxt1_params& dxt1_params, const qdxt5_params& dxt5_params, pixel_format fmt, bool cook) {
  if (!is_valid())
    return false;

  state.m_qdxt1_params = dxt1_params;
  state.m_qdxt5_params[0] = dxt5_params;
  state.m_qdxt5_params[1] = dxt5_params;
  utils::zero_object(state.m_has_blocks);

  switch (fmt) {
    case PIXEL_FMT_DXT1: {
      state.m_has_blocks[0] = true;
      break;
    }
    case PIXEL_FMT_DXT1A: {
      state.m_has_blocks[0] = true;
      state.m_qdxt1_params.m_use_alpha_blocks = true;
      break;
    }
    case PIXEL_FMT_DXT4:
    case PIXEL_FMT_DXT5: {
      state.m_has_blocks[0] = true;
      state.m_has_blocks[1] = true;
      state.m_qdxt1_params.m_use_alpha_blocks = false;
      state.m_qdxt5_params[0].m_comp_index = 3;
      break;
    }
    case PIXEL_FMT_DXT5_CCxY:
    case PIXEL_FMT_DXT5_xGxR:
    case PIXEL_FMT_DXT5_xGBR:
    case PIXEL_FMT_DXT5_AGBR: {
      state.m_has_blocks[0] = true;
      state.m_has_blocks[1] = true;
      state.m_qdxt1_params.m_use_alpha_blocks = false;
      state.m_qdxt1_params.m_perceptual = false;
      state.m_qdxt5_params[0].m_comp_index = 3;
      break;
    }
    case PIXEL_FMT_3DC: {
      state.m_has_blocks[1] = true;
      state.m_has_blocks[2] = true;
      state.m_qdxt5_params[0].m_comp_index = 1;
      state.m_qdxt5_params[1].m_comp_index = 0;
      break;
    }
    case PIXEL_FMT_DXN: {
      state.m_has_blocks[1] = true;
      state.m_has_blocks[2] = true;
      state.m_qdxt5_params[0].m_comp_index = 0;
      state.m_qdxt5_params[1].m_comp_index = 1;
      break;
    }
    case PIXEL_FMT_DXT5A: {
      state.m_has_blocks[1] = true;
      state.m_qdxt5_params[0].m_comp_index = 3;
      break;
    }
    case PIXEL_FMT_ETC1:
    case PIXEL_FMT_ETC2:
    case PIXEL_FMT_ETC2A:
    case PIXEL_FMT_ETC1S:
    case PIXEL_FMT_ETC2AS: {
      console::warning("mipmapped_texture::qdxt_pack_init: This method does not support ETCn");
      return false;
    }
    default: {
      CRNLIB_ASSERT(0);
      return false;
    }
  }

  const uint num_elements = state.m_has_blocks[0] + state.m_has_blocks[1] + state.m_has_blocks[2];

  uint cur_progress_start = dxt1_params.m_progress_start;
  if (state.m_has_blocks[0]) {
    state.m_qdxt1_params.m_progress_start = cur_progress_start;
    state.m_qdxt1_params.m_progress_range = dxt1_params.m_progress_range / num_elements;
    cur_progress_start += state.m_qdxt1_params.m_progress_range;
  }

  if (state.m_has_blocks[1]) {
    state.m_qdxt5_params[0].m_progress_start = cur_progress_start;
    state.m_qdxt5_params[0].m_progress_range = dxt1_params.m_progress_range / num_elements;
    cur_progress_start += state.m_qdxt5_params[0].m_progress_range;
  }

  if (state.m_has_blocks[2]) {
    state.m_qdxt5_params[1].m_progress_start = cur_progress_start;
    state.m_qdxt5_params[1].m_progress_range = dxt1_params.m_progress_range - cur_progress_start;
  }

  state.m_fmt = fmt;

  dst_tex.init(get_width(), get_height(), get_num_levels(), get_num_faces(), fmt, get_name().get_ptr(), cDefaultOrientationFlags);

  state.m_pixel_blocks.resize(0);

  image_utils::conversion_type cook_conv_type = image_utils::cConversion_Invalid;
  if (cook) {
    cook_conv_type = image_utils::get_conversion_type(true, fmt);
    if (pixel_format_helpers::is_alpha_only(fmt) && !pixel_format_helpers::has_alpha(m_format))
      cook_conv_type = image_utils::cConversion_Y_To_A;
  }

  state.m_qdxt1_params.m_num_mips = 0;
  state.m_qdxt5_params[0].m_num_mips = 0;
  state.m_qdxt5_params[1].m_num_mips = 0;

  for (uint f = 0; f < get_num_faces(); f++) {
    for (uint l = 0; l < get_num_levels(); l++) {
      mip_level* pLevel = get_level(f, l);

      dst_tex.get_level(f, l)->set_orientation_flags(pLevel->get_orientation_flags());

      image_u8 tmp_img;
      image_u8 img(*pLevel->get_unpacked_image(tmp_img, cUnpackFlagUncook));

      if (cook_conv_type != image_utils::cConversion_Invalid)
        image_utils::convert_image(img, cook_conv_type);

      const uint num_blocks_x = (img.get_width() + 3) / 4;
      const uint num_blocks_y = (img.get_height() + 3) / 4;
      const uint total_blocks = num_blocks_x * num_blocks_y;

      const uint cur_size = state.m_pixel_blocks.size();
      state.m_pixel_blocks.resize(cur_size + total_blocks);
      dxt_pixel_block* pDst_blocks = &state.m_pixel_blocks[cur_size];

      {
        CRNLIB_ASSERT(state.m_qdxt1_params.m_num_mips < qdxt1_params::cMaxMips);
        qdxt1_params::mip_desc& mip_desc = state.m_qdxt1_params.m_mip_desc[state.m_qdxt1_params.m_num_mips];
        mip_desc.m_first_block = cur_size;
        mip_desc.m_block_width = num_blocks_x;
        mip_desc.m_block_height = num_blocks_y;
        state.m_qdxt1_params.m_num_mips++;
      }

      for (uint i = 0; i < 2; i++) {
        CRNLIB_ASSERT(state.m_qdxt5_params[i].m_num_mips < qdxt5_params::cMaxMips);
        qdxt5_params::mip_desc& mip_desc = state.m_qdxt5_params[i].m_mip_desc[state.m_qdxt5_params[i].m_num_mips];
        mip_desc.m_first_block = cur_size;
        mip_desc.m_block_width = num_blocks_x;
        mip_desc.m_block_height = num_blocks_y;
        state.m_qdxt5_params[i].m_num_mips++;
      }

      for (uint block_y = 0; block_y < num_blocks_y; block_y++) {
        const uint img_y = block_y << 2;

        for (uint block_x = 0; block_x < num_blocks_x; block_x++) {
          const uint img_x = block_x << 2;

          color_quad_u8* pDst_pixel = &pDst_blocks->m_pixels[0][0];

          pDst_blocks++;

          for (uint by = 0; by < 4; by++)
            for (uint bx = 0; bx < 4; bx++)
              *pDst_pixel++ = img.get_clamped(img_x + bx, img_y + by);
        }  // block_x
      }    // block_y
    }      // l
  }        // f

  if (state.m_has_blocks[0]) {
    if (!state.m_qdxt1.init(state.m_pixel_blocks.size(), &state.m_pixel_blocks[0], state.m_qdxt1_params))
      return false;
  }

  if (state.m_has_blocks[1]) {
    if (!state.m_qdxt5a.init(state.m_pixel_blocks.size(), &state.m_pixel_blocks[0], state.m_qdxt5_params[0]))
      return false;
  }

  if (state.m_has_blocks[2]) {
    if (!state.m_qdxt5b.init(state.m_pixel_blocks.size(), &state.m_pixel_blocks[0], state.m_qdxt5_params[1]))
      return false;
  }

  return true;
}

bool mipmapped_texture::qdxt_pack(qdxt_state& state, mipmapped_texture& dst_tex, const qdxt1_params& dxt1_params, const qdxt5_params& dxt5_params) {
  if (!is_valid())
    return false;

  CRNLIB_ASSERT(dxt1_params.m_quality_level <= qdxt1_params::cMaxQuality);
  CRNLIB_ASSERT(dxt5_params.m_quality_level <= qdxt5_params::cMaxQuality);

  state.m_qdxt1_params.m_quality_level = dxt1_params.m_quality_level;
  state.m_qdxt1_params.m_pProgress_func = dxt1_params.m_pProgress_func;
  state.m_qdxt1_params.m_pProgress_data = dxt1_params.m_pProgress_data;

  state.m_qdxt5_params[0].m_quality_level = dxt5_params.m_quality_level;
  state.m_qdxt5_params[0].m_pProgress_func = dxt5_params.m_pProgress_func;
  state.m_qdxt5_params[0].m_pProgress_data = dxt5_params.m_pProgress_data;

  state.m_qdxt5_params[1].m_quality_level = dxt5_params.m_quality_level;
  state.m_qdxt5_params[1].m_pProgress_func = dxt5_params.m_pProgress_func;
  state.m_qdxt5_params[1].m_pProgress_data = dxt5_params.m_pProgress_data;

  const uint num_elements = state.m_has_blocks[0] + state.m_has_blocks[1] + state.m_has_blocks[2];

  uint cur_progress_start = dxt1_params.m_progress_start;
  if (state.m_has_blocks[0]) {
    state.m_qdxt1_params.m_progress_start = cur_progress_start;
    state.m_qdxt1_params.m_progress_range = dxt1_params.m_progress_range / num_elements;
    cur_progress_start += state.m_qdxt1_params.m_progress_range;
  }

  if (state.m_has_blocks[1]) {
    state.m_qdxt5_params[0].m_progress_start = cur_progress_start;
    state.m_qdxt5_params[0].m_progress_range = dxt1_params.m_progress_range / num_elements;
    cur_progress_start += state.m_qdxt5_params[0].m_progress_range;
  }

  if (state.m_has_blocks[2]) {
    state.m_qdxt5_params[1].m_progress_start = cur_progress_start;
    state.m_qdxt5_params[1].m_progress_range = dxt1_params.m_progress_range - cur_progress_start;
  }

  crnlib::vector<dxt1_block> dxt1_blocks;
  if (state.m_has_blocks[0]) {
    dxt1_blocks.resize(state.m_pixel_blocks.size());
    float pow_mul = 1.0f;

    if (state.m_fmt == PIXEL_FMT_DXT5_CCxY) {
      // use a "deeper" codebook size curves when compressing chroma into DXT1, because it's not as important
      pow_mul = 1.5f;
    } else if (state.m_fmt == PIXEL_FMT_DXT5) {
      // favor color more than alpha
      pow_mul = .75f;
    }

    if (!state.m_qdxt1.pack(&dxt1_blocks[0], 1, state.m_qdxt1_params, pow_mul))
      return false;
  }

  crnlib::vector<dxt5_block> dxt5_blocks[2];
  for (uint i = 0; i < 2; i++) {
    if (state.m_has_blocks[i + 1]) {
      dxt5_blocks[i].resize(state.m_pixel_blocks.size());

      if (!(i ? state.m_qdxt5b : state.m_qdxt5a).pack(&dxt5_blocks[i][0], 1, state.m_qdxt5_params[i]))
        return false;
    }
  }

  uint cur_block_ofs = 0;

  for (uint f = 0; f < dst_tex.get_num_faces(); f++) {
    for (uint l = 0; l < dst_tex.get_num_levels(); l++) {
      mip_level* pDst_level = dst_tex.get_level(f, l);

      const uint num_blocks_x = (pDst_level->get_width() + 3) / 4;
      const uint num_blocks_y = (pDst_level->get_height() + 3) / 4;
      const uint total_blocks = num_blocks_x * num_blocks_y;

      dxt_image* pDst_dxt_image = pDst_level->get_dxt_image();

      dxt_image::element* pDst = pDst_dxt_image->get_element_ptr();
      for (uint block_index = 0; block_index < total_blocks; block_index++) {
        if (state.m_has_blocks[1])
          memcpy(pDst, &dxt5_blocks[0][cur_block_ofs + block_index], 8);

        if (state.m_has_blocks[2])
          memcpy(pDst + 1, &dxt5_blocks[1][cur_block_ofs + block_index], 8);

        if (state.m_has_blocks[0])
          memcpy(pDst + state.m_has_blocks[1], &dxt1_blocks[cur_block_ofs + block_index], 8);

        pDst += pDst_dxt_image->get_elements_per_block();
      }

      cur_block_ofs += total_blocks;
    }
  }

  if (dxt1_params.m_pProgress_func) {
    if (!dxt1_params.m_pProgress_func(dxt1_params.m_progress_start + dxt1_params.m_progress_range, dxt1_params.m_pProgress_data))
      return false;
  }

  CRNLIB_ASSERT(dst_tex.check());

  return true;
}

bool mipmapped_texture::read_from_file(const char* pFilename, texture_file_types::format file_format) {
  clear();

  set_last_error("Can't open file");

  bool success = false;

  cfile_stream in_stream;
  if (in_stream.open(pFilename)) {
    data_stream_serializer serializer(in_stream);
    success = read_from_stream(serializer, file_format);
  }

  return success;
}

bool mipmapped_texture::read_from_stream(data_stream_serializer& serializer, texture_file_types::format file_format) {
  clear();

  if (!serializer.get_stream()) {
    set_last_error("Invalid stream");
    return false;
  }

  if (file_format == texture_file_types::cFormatInvalid)
    file_format = texture_file_types::determine_file_format(serializer.get_name().get_ptr());

  if (file_format == texture_file_types::cFormatInvalid) {
    set_last_error("Unsupported file format");
    return false;
  }

  set_last_error("Image file load failed");

  bool success = false;

  if (!texture_file_types::supports_mipmaps(file_format)) {
    success = read_regular_image(serializer);
  } else {
    switch (file_format) {
      case texture_file_types::cFormatDDS: {
        success = read_dds(serializer);
        break;
      }
      case texture_file_types::cFormatCRN: {
        success = read_crn(serializer);
        break;
      }
      case texture_file_types::cFormatKTX: {
        success = read_ktx(serializer);
        break;
      }
      default: {
        CRNLIB_ASSERT(0);
        break;
      }
    }
  }

  if (success) {
    CRNLIB_ASSERT(check());

    m_source_file_type = file_format;
    set_name(serializer.get_name());
    clear_last_error();
  }

  return success;
}

bool mipmapped_texture::read_regular_image(data_stream_serializer& serializer) {
  image_u8* pImg = crnlib_new<image_u8>();
  bool status = image_utils::read_from_stream(*pImg, serializer, 0);
  if (!status) {
    crnlib_delete(pImg);

    set_last_error("Failed loading image file");
    return false;
  }

  mip_level* pLevel = crnlib_new<mip_level>();
  pLevel->assign(pImg);

  assign(pLevel);
  set_name(serializer.get_name());

  return true;
}

bool mipmapped_texture::read_crn_from_memory(const void* pData, uint data_size, const char* pFilename) {
  clear();

  set_last_error("Image file load failed");

  if ((!pData) || (data_size < 1))
    return false;

  crnd::crn_texture_info tex_info;
  tex_info.m_struct_size = sizeof(crnd::crn_texture_info);
  if (!crnd_get_texture_info(pData, data_size, &tex_info)) {
    set_last_error("crnd_get_texture_info() failed");
    return false;
  }

  const pixel_format dds_fmt = (pixel_format)crnd::crnd_crn_format_to_fourcc(tex_info.m_format);
  if (dds_fmt == PIXEL_FMT_INVALID) {
    set_last_error("Unsupported DXT format");
    return false;
  }

  const dxt_format dxt_fmt = pixel_format_helpers::get_dxt_format(dds_fmt);

  face_vec faces(tex_info.m_faces);
  for (uint f = 0; f < tex_info.m_faces; f++) {
    faces[f].resize(tex_info.m_levels);

    for (uint l = 0; l < tex_info.m_levels; l++)
      faces[f][l] = crnlib_new<mip_level>();
  }

  const uint tex_num_blocks_x = (tex_info.m_width + 3) >> 2;
  const uint tex_num_blocks_y = (tex_info.m_height + 3) >> 2;

  vector<uint8> dxt_data;
  // Create temp buffer big enough to hold the largest mip level, and all faces if it's a cubemap.
  dxt_data.resize(tex_info.m_bytes_per_block * tex_num_blocks_x * tex_num_blocks_y * tex_info.m_faces);

  set_last_error("CRN unpack failed");

#if 0
      timer t;
      double total_time = 0.0f;
      t.start();
#endif

  crnd::crnd_unpack_context pContext = crnd::crnd_unpack_begin(pData, data_size);

#if 0
      total_time += t.get_elapsed_secs();
#endif

  if (!pContext) {
    for (uint f = 0; f < faces.size(); f++)
      for (uint l = 0; l < faces[f].size(); l++)
        crnlib_delete(faces[f][l]);
    return false;
  }

  void* pFaces[cCRNMaxFaces];
  for (uint f = tex_info.m_faces; f < cCRNMaxFaces; f++)
    pFaces[f] = NULL;

  for (uint l = 0; l < tex_info.m_levels; l++) {
    const uint level_width = math::maximum<uint>(1U, tex_info.m_width >> l);
    const uint level_height = math::maximum<uint>(1U, tex_info.m_height >> l);
    const uint num_blocks_x = (level_width + 3U) >> 2U;
    const uint num_blocks_y = (level_height + 3U) >> 2U;

    const uint row_pitch = num_blocks_x * tex_info.m_bytes_per_block;
    const uint size_of_face = num_blocks_y * row_pitch;

#if 0
         t.start();
#endif

    for (uint f = 0; f < tex_info.m_faces; f++)
      pFaces[f] = &dxt_data[f * size_of_face];

    if (!crnd::crnd_unpack_level(pContext, pFaces, dxt_data.size(), row_pitch, l)) {
      crnd::crnd_unpack_end(pContext);
      for (uint f = 0; f < faces.size(); f++)
        for (uint l = 0; l < faces[f].size(); l++)
          crnlib_delete(faces[f][l]);
      return false;
    }

#if 0
         total_time += t.get_elapsed_secs();
#endif

    for (uint f = 0; f < tex_info.m_faces; f++) {
      dxt_image* pDXT_image = crnlib_new<dxt_image>();

      if (!pDXT_image->init(
              dxt_fmt, level_width, level_height,
              num_blocks_x * num_blocks_y * (tex_info.m_bytes_per_block / sizeof(dxt_image::element)),
              reinterpret_cast<dxt_image::element*>(pFaces[f]), true)) {
        crnlib_delete(pDXT_image);

        crnd::crnd_unpack_end(pContext);
        for (uint f = 0; f < faces.size(); f++)
          for (uint l = 0; l < faces[f].size(); l++)
            crnlib_delete(faces[f][l]);

        return false;
      }

      faces[f][l]->assign(pDXT_image, dds_fmt);
    }
  }

#if 0
      if (total_pixels)
      {
         console::info("read_crn_from_memory: Total pixels: %u, ms: %3.3fms, megapixels/sec: %3.3f",
            total_pixels, total_time * 1000.0f, total_pixels / total_time);
      }
#endif

  crnd::crnd_unpack_end(pContext);

  assign(faces);
  set_name(pFilename);

  m_source_file_type = texture_file_types::cFormatCRN;
  clear_last_error();

  return true;
}

bool mipmapped_texture::read_crn(data_stream_serializer& serializer) {
  crnlib::vector<uint8> crn_data;
  if (!serializer.read_entire_file(crn_data)) {
    set_last_error("Failed reading CRN file");
    return false;
  }
  return read_crn_from_memory(crn_data.get_ptr(), crn_data.size(), serializer.get_name().get_ptr());
}

bool mipmapped_texture::write_to_file(
    const char* pFilename,
    texture_file_types::format file_format,
    crn_comp_params* pComp_params,
    uint32* pActual_quality_level, float* pActual_bitrate,
    uint32 image_write_flags) {
  if (pActual_quality_level)
    *pActual_quality_level = 0;
  if (pActual_bitrate)
    *pActual_bitrate = 0.0f;

  if (!is_valid()) {
    set_last_error("Unable to write empty texture");
    return false;
  }

  if (file_format == texture_file_types::cFormatInvalid)
    file_format = texture_file_types::determine_file_format(pFilename);

  if (file_format == texture_file_types::cFormatInvalid) {
    set_last_error("Unknown file format");
    return false;
  }

  bool success = false;

  if (((pComp_params) && (file_format == texture_file_types::cFormatDDS)) ||
      (file_format == texture_file_types::cFormatCRN)) {
    if (!pComp_params)
      return false;
    success = write_comp_texture(pFilename, *pComp_params, pActual_quality_level, pActual_bitrate);
  } else if (!texture_file_types::supports_mipmaps(file_format)) {
    success = write_regular_image(pFilename, image_write_flags);
  } else {
    if (pComp_params) {
      console::warning("mipmapped_texture::write_to_file: Ignoring CRN compression parameters (currently unsupported for this file type).");
    }

    cfile_stream write_stream;
    if (!write_stream.open(pFilename, cDataStreamWritable | cDataStreamSeekable)) {
      set_last_error(dynamic_string(cVarArg, "Failed creating output file \"%s\"", pFilename).get_ptr());
      return false;
    }
    data_stream_serializer serializer(write_stream);

    switch (file_format) {
      case texture_file_types::cFormatDDS: {
        success = write_dds(serializer);
        break;
      }
      case texture_file_types::cFormatKTX: {
        success = write_ktx(serializer);
        break;
      }
      default: {
        break;
      }
    }
  }

  return success;
}

bool mipmapped_texture::write_regular_image(const char* pFilename, uint32 image_write_flags) {
  image_u8 tmp;
  image_u8* pLevel_image = get_level_image(0, 0, tmp);

  if (!image_utils::write_to_file(pFilename, *pLevel_image, image_write_flags)) {
    set_last_error("File write failed");
    return false;
  }

  return true;
}

void mipmapped_texture::print_crn_comp_params(const crn_comp_params& p) {
  console::debug("CRN compression params:");
  console::debug("      File Type: %s", crn_get_file_type_ext(p.m_file_type));
  console::debug("  Quality level: %u", p.m_quality_level);
  console::debug(" Target Bitrate: %f", p.m_target_bitrate);
  console::debug("          Faces: %u", p.m_faces);
  console::debug("          Width: %u", p.m_width);
  console::debug("         Height: %u", p.m_height);
  console::debug("         Levels: %u", p.m_levels);
  console::debug("   Pixel Format: %s", crn_get_format_string(p.m_format));
  console::debug("Use manual CRN palette sizes: %u", p.get_flag(cCRNCompFlagManualPaletteSizes));
  console::debug("Color endpoints: %u", p.m_crn_color_endpoint_palette_size);
  console::debug("Color selectors: %u", p.m_crn_color_selector_palette_size);
  console::debug("Alpha endpoints: %u", p.m_crn_alpha_endpoint_palette_size);
  console::debug("Alpha selectors: %u", p.m_crn_alpha_selector_palette_size);
  console::debug("Flags:");
  console::debug("    Perceptual: %u", p.get_flag(cCRNCompFlagPerceptual));
  console::debug("  Hierarchical: %u", p.get_flag(cCRNCompFlagHierarchical));
  console::debug("  UseBothBlockTypes: %u", p.get_flag(cCRNCompFlagUseBothBlockTypes));
  console::debug("  UseTransparentIndicesForBlack: %u", p.get_flag(cCRNCompFlagUseTransparentIndicesForBlack));
  console::debug("  DisableEndpointCaching: %u", p.get_flag(cCRNCompFlagDisableEndpointCaching));
  console::debug("GrayscaleSampling: %u", p.get_flag(cCRNCompFlagGrayscaleSampling));
  console::debug("  UseDXT1ATransparency: %u", p.get_flag(cCRNCompFlagDXT1AForTransparency));
  console::debug("AdaptiveTileColorPSNRDerating: %2.2fdB", p.m_crn_adaptive_tile_color_psnr_derating);
  console::debug("AdaptiveTileAlphaPSNRDerating: %2.2fdB", p.m_crn_adaptive_tile_alpha_psnr_derating);
  console::debug("NumHelperThreads: %u", p.m_num_helper_threads);
}

bool mipmapped_texture::write_comp_texture(const char* pFilename, const crn_comp_params& orig_comp_params, uint32* pActual_quality_level, float* pActual_bitrate) {
  crn_comp_params comp_params(orig_comp_params);

  if (pActual_quality_level)
    *pActual_quality_level = 0;
  if (pActual_bitrate)
    *pActual_bitrate = 0.0f;

  if (math::maximum(get_height(), get_width()) > cCRNMaxLevelResolution) {
    set_last_error("Texture resolution is too big!");
    return false;
  }

  comp_params.m_faces = get_num_faces();
  comp_params.m_levels = get_num_levels();
  comp_params.m_width = get_width();
  comp_params.m_height = get_height();

  image_u8 temp_images[cCRNMaxFaces][cCRNMaxLevels];
  for (uint f = 0; f < get_num_faces(); f++) {
    for (uint l = 0; l < get_num_levels(); l++) {
      image_u8* p = get_level_image(f, l, temp_images[f][l]);

      comp_params.m_pImages[f][l] = (crn_uint32*)p->get_ptr();
    }
  }

  if (comp_params.get_flag(cCRNCompFlagDebugging))
    print_crn_comp_params(comp_params);

  timer t;
  t.start();

  crnlib::vector<uint8> comp_data;
  if (!create_compressed_texture(comp_params, comp_data, pActual_quality_level, pActual_bitrate)) {
    set_last_error("CRN compression failed");
    return false;
  }

  double total_time = t.get_elapsed_secs();
  if (comp_params.get_flag(cCRNCompFlagDebugging)) {
    console::debug("\nTotal compression time: %3.3fs", total_time);
  }

  cfile_stream out_stream;
  if (!out_stream.open(pFilename, cDataStreamWritable | cDataStreamSeekable)) {
    set_last_error("Failed opening file");
    return false;
  }

  if (out_stream.write(comp_data.get_ptr(), comp_data.size()) != comp_data.size()) {
    set_last_error("Failed writing to file");
    return false;
  }

  if (!out_stream.close()) {
    set_last_error("Failed writing to file");
    return false;
  }

  return true;
}

uint mipmapped_texture::get_total_pixels_in_all_faces_and_mips() const {
  uint total_pixels = 0;
  for (uint l = 0; l < m_faces.size(); l++)
    for (uint m = 0; m < m_faces[l].size(); m++)
      total_pixels += m_faces[l][m]->get_total_pixels();

  return total_pixels;
}

void mipmapped_texture::set_orientation_flags(orientation_flags_t flags) {
  for (uint l = 0; l < m_faces.size(); l++)
    for (uint m = 0; m < m_faces[l].size(); m++)
      m_faces[l][m]->set_orientation_flags(flags);
}

bool mipmapped_texture::is_flipped() const {
  for (uint l = 0; l < m_faces.size(); l++)
    for (uint m = 0; m < m_faces[l].size(); m++)
      if (m_faces[l][m]->is_flipped())
        return true;

  return false;
}

bool mipmapped_texture::is_x_flipped() const {
  for (uint l = 0; l < m_faces.size(); l++)
    for (uint m = 0; m < m_faces[l].size(); m++)
      if (m_faces[l][m]->is_x_flipped())
        return true;

  return false;
}

bool mipmapped_texture::is_y_flipped() const {
  for (uint l = 0; l < m_faces.size(); l++)
    for (uint m = 0; m < m_faces[l].size(); m++)
      if (m_faces[l][m]->is_y_flipped())
        return true;

  return false;
}

bool mipmapped_texture::can_unflip_without_unpacking() const {
  if (!is_valid())
    return false;

  if (!is_packed())
    return true;

  for (uint l = 0; l < m_faces.size(); l++)
    for (uint m = 0; m < m_faces[l].size(); m++)
      if (!m_faces[l][m]->can_unflip_without_unpacking())
        return false;

  return true;
}

bool mipmapped_texture::unflip(bool allow_unpacking_to_flip, bool uncook_if_necessary_to_unpack) {
  if (!is_valid())
    return false;

  if (is_packed()) {
    // The texture is packed - make sure all faces/miplevels can be consistently unflipped.
    bool can_do_packed_unflip = can_unflip_without_unpacking();

    if ((!can_do_packed_unflip) && (!allow_unpacking_to_flip))
      return false;

    // If any face/miplevel can't unflip the packed bits, then just unpack the whole texture.
    if (!can_do_packed_unflip)
      unpack_from_dxt(uncook_if_necessary_to_unpack);
  }

  for (uint l = 0; l < m_faces.size(); l++)
    for (uint m = 0; m < m_faces[l].size(); m++)
      if (!m_faces[l][m]->unflip(true, false))
        return false;

  CRNLIB_VERIFY(check());

  return true;
}

#if 0
   bool mipmapped_texture::flip_x()
   {
      for (uint l = 0; l < m_faces.size(); l++)
         for (uint m = 0; m < m_faces[l].size(); m++) 
            if (!m_faces[l][m]->flip_x())
               return false;

      return true;
   }
#endif

bool mipmapped_texture::flip_y_helper() {
  for (uint l = 0; l < m_faces.size(); l++)
    for (uint m = 0; m < m_faces[l].size(); m++)
      if (!m_faces[l][m]->flip_y())
        return false;

  return true;
}

bool mipmapped_texture::flip_y(bool update_orientation_flags) {
  mipmapped_texture temp_tex(*this);
  if (!temp_tex.flip_y_helper()) {
    temp_tex = *this;
    temp_tex.unpack_from_dxt(true);
    if (!temp_tex.flip_y_helper())
      return false;
  }
  swap(temp_tex);

  if (update_orientation_flags) {
    for (uint f = 0; f < get_num_faces(); f++) {
      for (uint m = 0; m < get_face(f).size(); m++) {
        uint orient_flags = get_face(f)[m]->get_orientation_flags();
        orient_flags ^= cOrientationFlagYFlipped;
        get_face(f)[m]->set_orientation_flags(static_cast<orientation_flags_t>(orient_flags));
      }
    }
  }

  CRNLIB_ASSERT(check());

  return true;
}

}  // namespace crnlib
