// File: crn_mipmapped_texture.h
// See Copyright Notice and license at the end of inc/crnlib.h
#pragma once
#include "crn_dxt_image.h"
#include "../inc/dds_defs.h"
#include "crn_pixel_format.h"
#include "crn_image.h"
#include "crn_resampler.h"
#include "crn_data_stream_serializer.h"
#include "crn_qdxt1.h"
#include "crn_qdxt5.h"
#include "crn_texture_file_types.h"
#include "crn_image_utils.h"

namespace crnlib {
extern const vec2I g_vertical_cross_image_offsets[6];

enum orientation_flags_t {
  cOrientationFlagXFlipped = 1,
  cOrientationFlagYFlipped = 2,

  cDefaultOrientationFlags = 0
};

enum unpack_flags_t {
  cUnpackFlagUncook = 1,
  cUnpackFlagUnflip = 2
};

class mip_level {
  friend class mipmapped_texture;

 public:
  mip_level();
  ~mip_level();

  mip_level(const mip_level& other);
  mip_level& operator=(const mip_level& rhs);

  // Assumes ownership.
  void assign(image_u8* p, pixel_format fmt = PIXEL_FMT_INVALID, orientation_flags_t orient_flags = cDefaultOrientationFlags);
  void assign(dxt_image* p, pixel_format fmt = PIXEL_FMT_INVALID, orientation_flags_t orient_flags = cDefaultOrientationFlags);

  void clear();

  inline uint get_width() const { return m_width; }
  inline uint get_height() const { return m_height; }
  inline uint get_total_pixels() const { return m_width * m_height; }

  orientation_flags_t get_orientation_flags() const { return m_orient_flags; }
  void set_orientation_flags(orientation_flags_t flags) { m_orient_flags = flags; }

  inline image_u8* get_image() const { return m_pImage; }
  inline dxt_image* get_dxt_image() const { return m_pDXTImage; }

  image_u8* get_unpacked_image(image_u8& tmp, uint unpack_flags) const;

  inline bool is_packed() const { return m_pDXTImage != NULL; }

  inline bool is_valid() const { return (m_pImage != NULL) || (m_pDXTImage != NULL); }

  inline pixel_format_helpers::component_flags get_comp_flags() const { return m_comp_flags; }
  inline void set_comp_flags(pixel_format_helpers::component_flags comp_flags) { m_comp_flags = comp_flags; }

  inline pixel_format get_format() const { return m_format; }
  inline void set_format(pixel_format fmt) { m_format = fmt; }

  bool convert(pixel_format fmt, bool cook, const dxt_image::pack_params& p);

  bool pack_to_dxt(const image_u8& img, pixel_format fmt, bool cook, const dxt_image::pack_params& p, orientation_flags_t orient_flags = cDefaultOrientationFlags);
  bool pack_to_dxt(pixel_format fmt, bool cook, const dxt_image::pack_params& p);

  bool unpack_from_dxt(bool uncook = true);

  // Returns true if flipped on either axis.
  bool is_flipped() const;

  bool is_x_flipped() const;
  bool is_y_flipped() const;

  bool can_unflip_without_unpacking() const;

  // Returns true if unflipped on either axis.
  // Will try to flip packed (DXT/ETC) data in-place, if this isn't possible it'll unpack/uncook the mip level then unflip.
  bool unflip(bool allow_unpacking_to_flip, bool uncook_during_unpack);

  bool set_alpha_to_luma();
  bool convert(image_utils::conversion_type conv_type);

  bool flip_x();
  bool flip_y();

 private:
  uint m_width;
  uint m_height;

  pixel_format_helpers::component_flags m_comp_flags;
  pixel_format m_format;

  image_u8* m_pImage;
  dxt_image* m_pDXTImage;

  orientation_flags_t m_orient_flags;

  void cook_image(image_u8& img) const;
  void uncook_image(image_u8& img) const;
};

// A face is an array of mip_level ptr's.
typedef crnlib::vector<mip_level*> mip_ptr_vec;

// And an array of one, six, or N faces make up a texture.
typedef crnlib::vector<mip_ptr_vec> face_vec;

class mipmapped_texture {
 public:
  // Construction/destruction
  mipmapped_texture();
  ~mipmapped_texture();

  mipmapped_texture(const mipmapped_texture& other);
  mipmapped_texture& operator=(const mipmapped_texture& rhs);

  void clear();

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

  // Assumes ownership.
  void assign(face_vec& faces);
  void assign(mip_level* pLevel);
  void assign(image_u8* p, pixel_format fmt = PIXEL_FMT_INVALID, orientation_flags_t orient_flags = cDefaultOrientationFlags);
  void assign(dxt_image* p, pixel_format fmt = PIXEL_FMT_INVALID, orientation_flags_t orient_flags = cDefaultOrientationFlags);

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

  // Accessors
  image_u8* get_level_image(uint face, uint level, image_u8& img, uint unpack_flags = cUnpackFlagUncook | cUnpackFlagUnflip) const;

  inline bool is_valid() const { return m_faces.size() > 0; }

  const dynamic_string& get_name() const { return m_name; }
  void set_name(const dynamic_string& name) { m_name = name; }

  const dynamic_string& get_source_filename() const { return get_name(); }
  texture_file_types::format get_source_file_type() const { return m_source_file_type; }

  inline uint get_width() const { return m_width; }
  inline uint get_height() const { return m_height; }
  inline uint get_total_pixels() const { return m_width * m_height; }
  uint get_total_pixels_in_all_faces_and_mips() const;

  inline uint get_num_faces() const { return m_faces.size(); }
  inline uint get_num_levels() const {
    if (m_faces.empty())
      return 0;
    else
      return m_faces[0].size();
  }

  inline pixel_format_helpers::component_flags get_comp_flags() const { return m_comp_flags; }
  inline pixel_format get_format() const { return m_format; }

  inline bool is_unpacked() const {
    if (get_num_faces()) {
      return get_level(0, 0)->get_image() != NULL;
    }
    return false;
  }

  inline const mip_ptr_vec& get_face(uint face) const { return m_faces[face]; }
  inline mip_ptr_vec& get_face(uint face) { return m_faces[face]; }

  inline const mip_level* get_level(uint face, uint mip) const { return m_faces[face][mip]; }
  inline mip_level* get_level(uint face, uint mip) { return m_faces[face][mip]; }

  bool has_alpha() const;
  bool is_normal_map() const;
  bool is_vertical_cross() const;
  bool is_packed() const;
  texture_type determine_texture_type(bool no_normal_detection) const;

  const dynamic_string& get_last_error() const { return m_last_error; }
  void clear_last_error() { m_last_error.clear(); }

  // Reading/writing
  bool read_dds(data_stream_serializer& serializer);
  bool write_dds(data_stream_serializer& serializer) const;

  bool read_ktx(data_stream_serializer& serializer);
  bool write_ktx(data_stream_serializer& serializer) const;

  bool read_crn(data_stream_serializer& serializer);
  bool read_crn_from_memory(const void* pData, uint data_size, const char* pFilename);

  // If file_format is texture_file_types::cFormatInvalid, the format will be determined from the filename's extension.
  bool read_from_file(const char* pFilename, texture_file_types::format file_format = texture_file_types::cFormatInvalid);
  bool read_from_stream(data_stream_serializer& serializer, texture_file_types::format file_format = texture_file_types::cFormatInvalid);

  bool write_to_file(
      const char* pFilename,
      texture_file_types::format file_format = texture_file_types::cFormatInvalid,
      crn_comp_params* pComp_params = NULL,
      uint32* pActual_quality_level = NULL, float* pActual_bitrate = NULL,
      uint32 image_write_flags = 0);

  // Conversion
  bool convert(pixel_format fmt, bool cook, const dxt_image::pack_params& p);
  bool convert(pixel_format fmt, const dxt_image::pack_params& p);
  bool convert(pixel_format fmt, bool cook, const dxt_image::pack_params& p, int qdxt_quality, bool hierarchical = true);
  bool convert(image_utils::conversion_type conv_type);

  bool unpack_from_dxt(bool uncook = true);

  bool set_alpha_to_luma();

  void discard_mipmaps();

  void discard_mips();

  struct resample_params {
    resample_params()
        : m_pFilter("kaiser"),
          m_wrapping(false),
          m_srgb(false),
          m_renormalize(false),
          m_rtopmip(false),
          m_filter_scale(.9f),
          m_gamma(1.75f),  // or 2.2f
          m_multithreaded(true) {
    }

    const char* m_pFilter;
    bool m_wrapping;
    bool m_srgb;
    bool m_renormalize;
    bool m_rtopmip;
    float m_filter_scale;
    float m_gamma;
    bool m_multithreaded;
  };

  bool resize(uint new_width, uint new_height, const resample_params& params);

  struct generate_mipmap_params : public resample_params {
    generate_mipmap_params()
        : resample_params(),
          m_min_mip_size(1),
          m_max_mips(0) {
    }

    uint m_min_mip_size;
    uint m_max_mips;  // actually the max # of total levels
  };

  bool generate_mipmaps(const generate_mipmap_params& params, bool force);

  bool crop(uint x, uint y, uint width, uint height);

  bool vertical_cross_to_cubemap();

  // Low-level clustered DXT (QDXT) compression
  struct qdxt_state {
    qdxt_state(task_pool& tp)
        : m_fmt(PIXEL_FMT_INVALID), m_qdxt1(tp), m_qdxt5a(tp), m_qdxt5b(tp) {
    }

    pixel_format m_fmt;
    qdxt1 m_qdxt1;
    qdxt5 m_qdxt5a;
    qdxt5 m_qdxt5b;
    crnlib::vector<dxt_pixel_block> m_pixel_blocks;

    qdxt1_params m_qdxt1_params;
    qdxt5_params m_qdxt5_params[2];
    bool m_has_blocks[3];

    void clear() {
      m_fmt = PIXEL_FMT_INVALID;
      m_qdxt1.clear();
      m_qdxt5a.clear();
      m_qdxt5b.clear();
      m_pixel_blocks.clear();
      m_qdxt1_params.clear();
      m_qdxt5_params[0].clear();
      m_qdxt5_params[1].clear();
      utils::zero_object(m_has_blocks);
    }
  };
  bool 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);
  bool qdxt_pack(qdxt_state& state, mipmapped_texture& dst_tex, const qdxt1_params& dxt1_params, const qdxt5_params& dxt5_params);

  void swap(mipmapped_texture& img);

  bool check() const;

  void set_orientation_flags(orientation_flags_t flags);

  // Returns true if any face/miplevel is flipped.
  bool is_flipped() const;
  bool is_x_flipped() const;
  bool is_y_flipped() const;
  bool can_unflip_without_unpacking() const;
  bool unflip(bool allow_unpacking_to_flip, bool uncook_if_necessary_to_unpack);

  bool flip_y(bool update_orientation_flags);

 private:
  dynamic_string m_name;

  uint m_width;
  uint m_height;

  pixel_format_helpers::component_flags m_comp_flags;
  pixel_format m_format;

  face_vec m_faces;

  texture_file_types::format m_source_file_type;

  mutable dynamic_string m_last_error;

  inline void clear_last_error() const { m_last_error.clear(); }
  inline void set_last_error(const char* p) const { m_last_error = p; }

  void free_all_mips();
  bool read_regular_image(data_stream_serializer& serializer);
  bool write_regular_image(const char* pFilename, uint32 image_write_flags);
  bool read_dds_internal(data_stream_serializer& serializer);
  void print_crn_comp_params(const crn_comp_params& p);
  bool write_comp_texture(const char* pFilename, const crn_comp_params& comp_params, uint32* pActual_quality_level, float* pActual_bitrate);
  void change_dxt1_to_dxt1a();
  bool flip_y_helper();
};

inline void swap(mipmapped_texture& a, mipmapped_texture& b) {
  a.swap(b);
}

}  // namespace crnlib
