// File: crn_texture_comp.cpp
// See Copyright Notice and license at the end of inc/crnlib.h
#include "crn_core.h"
#include "crn_texture_comp.h"
#include "crn_dds_comp.h"
#include "crn_console.h"
#include "crn_rect.h"

namespace crnlib
{
   static itexture_comp *create_texture_comp(crn_file_type file_type)
   {
      if (file_type == cCRNFileTypeCRN)
         return crnlib_new<crn_comp>();
      else if (file_type == cCRNFileTypeDDS)
         return crnlib_new<dds_comp>();
      else
         return NULL;
   }

   bool create_compressed_texture(const crn_comp_params &params, crnlib::vector<uint8> &comp_data, uint32 *pActual_quality_level, float *pActual_bitrate)
   {
      crn_comp_params local_params(params);

      if (pixel_format_helpers::is_crn_format_non_srgb(local_params.m_format))
      {
         if (local_params.get_flag(cCRNCompFlagPerceptual))
         {
            console::info("Output pixel format is swizzled or not RGB, disabling perceptual color metrics");

            // Destination compressed pixel format is swizzled or not RGB at all, so be sure perceptual colorspace metrics are disabled.
            local_params.set_flag(cCRNCompFlagPerceptual, false);
         }
      }

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

      comp_data.resize(0);

      itexture_comp *pTexture_comp = create_texture_comp(local_params.m_file_type);
      if (!pTexture_comp)
         return false;

      if (!pTexture_comp->compress_init(local_params))
      {
         crnlib_delete(pTexture_comp);
         return false;
      }

      if ( (local_params.m_target_bitrate <= 0.0f) ||
           (local_params.m_format == cCRNFmtDXT3) ||
           ((local_params.m_file_type == cCRNFileTypeCRN) && ((local_params.m_flags & cCRNCompFlagManualPaletteSizes) != 0))
          )
      {
         if ( (local_params.m_file_type == cCRNFileTypeCRN) ||
              ((local_params.m_file_type == cCRNFileTypeDDS) && (local_params.m_quality_level < cCRNMaxQualityLevel)) )
         {
            console::info("Compressing using quality level %i", local_params.m_quality_level);
         }
         if (local_params.m_format == cCRNFmtDXT3)
         {
            if (local_params.m_file_type == cCRNFileTypeCRN)
               console::warning("CRN format doesn't support DXT3");
            else if ((local_params.m_file_type == cCRNFileTypeDDS) && (local_params.m_quality_level < cCRNMaxQualityLevel))
               console::warning("Clustered DDS compressor doesn't support DXT3");
         }
         if (!pTexture_comp->compress_pass(local_params, pActual_bitrate))
         {
            crnlib_delete(pTexture_comp);
            return false;
         }

         comp_data.swap(pTexture_comp->get_comp_data());

         if ((pActual_quality_level) && (local_params.m_target_bitrate <= 0.0))
            *pActual_quality_level = local_params.m_quality_level;

         crnlib_delete(pTexture_comp);
         return true;
      }

      // Interpolative search to find closest quality level to target bitrate.
      const int cLowestQuality = 0;
      const int cHighestQuality = cCRNMaxQualityLevel;
      const int cNumQualityLevels = cHighestQuality - cLowestQuality + 1;

      float best_bitrate = 1e+10f;
      int best_quality_level = -1;
      const uint cMaxIterations = 8;

      for ( ; ; )
      {
         int low_quality = cLowestQuality;
         int high_quality = cHighestQuality;

         float cached_bitrates[cNumQualityLevels];
         for (int i = 0; i < cNumQualityLevels; i++)
            cached_bitrates[i] = -1.0f;

         float highest_bitrate = 0.0f;

         uint iter_count = 0;
         bool force_binary_search = false;

         while (low_quality <= high_quality)
         {
            if (params.m_flags & cCRNCompFlagDebugging)
            {
               console::debug("Quality level bracket: [%u, %u]", low_quality, high_quality);
            }

            int trial_quality = (low_quality + high_quality) / 2;

            if ((iter_count) && (!force_binary_search))
            {
               int bracket_low = trial_quality;
               while ((cached_bitrates[bracket_low] < 0) && (bracket_low > cLowestQuality))
                  bracket_low--;

               if (cached_bitrates[bracket_low] < 0)
                  trial_quality = static_cast<int>(math::lerp<float>((float)low_quality, (float)high_quality, .33f));
               else
               {
                  int bracket_high = trial_quality + 1;
                  if (bracket_high <= cHighestQuality)
                  {
                     while ((cached_bitrates[bracket_high] < 0) && (bracket_high < cHighestQuality))
                        bracket_high++;

                     if (cached_bitrates[bracket_high] >= 0)
                     {
                        float bracket_low_bitrate = cached_bitrates[bracket_low];
                        float bracket_high_bitrate = cached_bitrates[bracket_high];

                        if ((bracket_low_bitrate < bracket_high_bitrate) &&
                           (bracket_low_bitrate < local_params.m_target_bitrate) &&
                           (bracket_high_bitrate >= local_params.m_target_bitrate))
                        {
                           int quality = low_quality + static_cast<int>( ((local_params.m_target_bitrate - bracket_low_bitrate) * (high_quality - low_quality)) / (bracket_high_bitrate - bracket_low_bitrate) );

                           if ((quality >= low_quality) && (quality <= high_quality))
                           {
                              trial_quality = quality;
                           }
                        }
                     }
                  }
               }
            }

            console::info("Compressing to quality level %u", trial_quality);

            float bitrate = 0.0f;

            local_params.m_quality_level = trial_quality;

            if (!pTexture_comp->compress_pass(local_params, &bitrate))
            {
               crnlib_delete(pTexture_comp);
               return false;
            }

            cached_bitrates[trial_quality] = bitrate;

            highest_bitrate = math::maximum(highest_bitrate, bitrate);

            console::info("\nTried quality level %u, bpp: %3.3f", trial_quality, bitrate);

            if ( (best_quality_level < 0) ||
                 ((bitrate <= local_params.m_target_bitrate) && (best_bitrate > local_params.m_target_bitrate)) ||
                 (((bitrate <= local_params.m_target_bitrate) || (best_bitrate > local_params.m_target_bitrate)) && (fabs(bitrate - local_params.m_target_bitrate) < fabs(best_bitrate - local_params.m_target_bitrate)))
                )
            {
               best_bitrate = bitrate;
               comp_data.swap(pTexture_comp->get_comp_data());
               best_quality_level = trial_quality;
               if (params.m_flags & cCRNCompFlagDebugging)
               {
                  console::debug("Choose new best quality level");
               }

               if ((best_bitrate <= local_params.m_target_bitrate) && (fabs(best_bitrate - local_params.m_target_bitrate) < .005f))
                  break;
            }

            if (bitrate > local_params.m_target_bitrate)
               high_quality = trial_quality - 1;
            else
               low_quality = trial_quality + 1;

            iter_count++;
            if (iter_count > cMaxIterations)
            {
               force_binary_search = true;
            }
         }

         if (((local_params.m_flags & cCRNCompFlagHierarchical) != 0) &&
            (highest_bitrate < local_params.m_target_bitrate) &&
            (fabs(best_bitrate - local_params.m_target_bitrate) >= .005f))
         {
            console::info("Unable to achieve desired bitrate - disabling adaptive block sizes and retrying search.");

            local_params.m_flags &= ~cCRNCompFlagHierarchical;

            crnlib_delete(pTexture_comp);
            pTexture_comp = create_texture_comp(local_params.m_file_type);

            if (!pTexture_comp->compress_init(local_params))
            {
               crnlib_delete(pTexture_comp);
               return false;
            }
         }
         else
            break;
      }

      crnlib_delete(pTexture_comp);
      pTexture_comp = NULL;

      if (best_quality_level < 0)
         return false;

      if (pActual_quality_level) *pActual_quality_level = best_quality_level;
      if (pActual_bitrate) *pActual_bitrate = best_bitrate;

      console::printf("Selected quality level %u bpp: %f", best_quality_level, best_bitrate);

      return true;
   }

   static bool create_dds_tex(const crn_comp_params &params, mipmapped_texture &dds_tex)
   {
      image_u8 images[cCRNMaxFaces][cCRNMaxLevels];

      bool has_alpha = false;
      for (uint face_index = 0; face_index < params.m_faces; face_index++)
      {
         for (uint level_index = 0; level_index < params.m_levels; level_index++)
         {
            const uint width = math::maximum(1U, params.m_width >> level_index);
            const uint height = math::maximum(1U, params.m_height >> level_index);

            if (!params.m_pImages[face_index][level_index])
               return false;

            images[face_index][level_index].alias((color_quad_u8*)params.m_pImages[face_index][level_index], width, height);
            if (!has_alpha)
               has_alpha = image_utils::has_alpha(images[face_index][level_index]);
         }
      }

      for (uint face_index = 0; face_index < params.m_faces; face_index++)
         for (uint level_index = 0; level_index < params.m_levels; level_index++)
            images[face_index][level_index].set_component_valid(3, has_alpha);

      face_vec faces(params.m_faces);

      for (uint face_index = 0; face_index < params.m_faces; face_index++)
      {
         for (uint level_index = 0; level_index < params.m_levels; level_index++)
         {
            mip_level *pMip = crnlib_new<mip_level>();

            image_u8 *pImage = crnlib_new<image_u8>();
            pImage->swap(images[face_index][level_index]);
            pMip->assign(pImage);

            faces[face_index].push_back(pMip);
         }
      }

      dds_tex.assign(faces);

#ifdef CRNLIB_BUILD_DEBUG
      CRNLIB_ASSERT(dds_tex.check());
#endif

      return true;
   }

   bool create_texture_mipmaps(mipmapped_texture &work_tex, const crn_comp_params &params, const crn_mipmap_params &mipmap_params, bool generate_mipmaps)
   {
      crn_comp_params new_params(params);

      bool generate_new_mips = false;

      switch (mipmap_params.m_mode)
      {
         case cCRNMipModeUseSourceOrGenerateMips:
         {
            if (work_tex.get_num_levels() == 1)
               generate_new_mips = true;
            break;
         }
         case cCRNMipModeUseSourceMips:
         {
            break;
         }
         case cCRNMipModeGenerateMips:
         {
            generate_new_mips = true;
            break;
         }
         case cCRNMipModeNoMips:
         {
            work_tex.discard_mipmaps();
            break;
         }
         default:
         {
            CRNLIB_ASSERT(0);
            break;
         }
      }

      rect window_rect(mipmap_params.m_window_left, mipmap_params.m_window_top, mipmap_params.m_window_right, mipmap_params.m_window_bottom);

      if (!window_rect.is_empty())
      {
         if (work_tex.get_num_faces() > 1)
         {
            console::warning("Can't crop cubemap textures");
         }
         else
         {
            console::info("Cropping input texture from window (%ux%u)-(%ux%u)", window_rect.get_left(), window_rect.get_top(), window_rect.get_right(), window_rect.get_bottom());

            if (!work_tex.crop(window_rect.get_left(), window_rect.get_top(), window_rect.get_width(), window_rect.get_height()))
               console::warning("Failed cropping window rect");
         }
      }

      int new_width = work_tex.get_width();
      int new_height = work_tex.get_height();

      if ((mipmap_params.m_clamp_width) && (mipmap_params.m_clamp_height))
      {
         if ((new_width > (int)mipmap_params.m_clamp_width) || (new_height > (int)mipmap_params.m_clamp_height))
         {
            if (!mipmap_params.m_clamp_scale)
            {
               if (work_tex.get_num_faces() > 1)
               {
                  console::warning("Can't crop cubemap textures");
               }
               else
               {
                  new_width = math::minimum<uint>(mipmap_params.m_clamp_width, new_width);
                  new_height = math::minimum<uint>(mipmap_params.m_clamp_height, new_height);
                  console::info("Clamping input texture to %ux%u", new_width, new_height);
                  work_tex.crop(0, 0, new_width, new_height);
               }
            }
         }
      }

      if (mipmap_params.m_scale_mode != cCRNSMDisabled)
      {
         bool is_pow2 = math::is_power_of_2((uint32)new_width) && math::is_power_of_2((uint32)new_height);

         switch (mipmap_params.m_scale_mode)
         {
            case cCRNSMAbsolute:
            {
               new_width = (uint)mipmap_params.m_scale_x;
               new_height = (uint)mipmap_params.m_scale_y;
               break;
            }
            case cCRNSMRelative:
            {
               new_width = (uint)(mipmap_params.m_scale_x * new_width + .5f);
               new_height = (uint)(mipmap_params.m_scale_y * new_height + .5f);
               break;
            }
            case cCRNSMLowerPow2:
            {
               if (!is_pow2)
                  math::compute_lower_pow2_dim(new_width, new_height);
               break;
            }
            case cCRNSMNearestPow2:
            {
               if (!is_pow2)
               {
                  int lwidth = new_width;
                  int lheight = new_height;
                  math::compute_lower_pow2_dim(lwidth, lheight);

                  int uwidth = new_width;
                  int uheight = new_height;
                  math::compute_upper_pow2_dim(uwidth, uheight);

                  if (labs(new_width - lwidth) < labs(new_width - uwidth))
                     new_width = lwidth;
                  else
                     new_width = uwidth;

                  if (labs(new_height - lheight) < labs(new_height - uheight))
                     new_height = lheight;
                  else
                     new_height = uheight;
               }
               break;
            }
            case cCRNSMNextPow2:
            {
               if (!is_pow2)
                  math::compute_upper_pow2_dim(new_width, new_height);
               break;
            }
            default: break;
         }
      }

      if ((mipmap_params.m_clamp_width) && (mipmap_params.m_clamp_height))
      {
         if ((new_width > (int)mipmap_params.m_clamp_width) || (new_height > (int)mipmap_params.m_clamp_height))
         {
            if (mipmap_params.m_clamp_scale)
            {
               new_width = math::minimum<uint>(mipmap_params.m_clamp_width, new_width);
               new_height = math::minimum<uint>(mipmap_params.m_clamp_height, new_height);
            }
         }
      }

      new_width = math::clamp<int>(new_width, 1, cCRNMaxLevelResolution);
      new_height = math::clamp<int>(new_height, 1, cCRNMaxLevelResolution);

      if ((new_width != (int)work_tex.get_width()) || (new_height != (int)work_tex.get_height()))
      {
         console::info("Resampling input texture to %ux%u", new_width, new_height);

         const char* pFilter = crn_get_mip_filter_name(mipmap_params.m_filter);

         bool srgb = mipmap_params.m_gamma_filtering != 0;

         mipmapped_texture::resample_params res_params;
         res_params.m_pFilter = pFilter;
         res_params.m_wrapping = mipmap_params.m_tiled != 0;
         if (work_tex.get_num_faces())
            res_params.m_wrapping = false;
         res_params.m_renormalize = mipmap_params.m_renormalize != 0;
         res_params.m_filter_scale = 1.0f;
         res_params.m_gamma = mipmap_params.m_gamma;
         res_params.m_srgb = srgb;
         res_params.m_multithreaded = (params.m_num_helper_threads > 0);

         if (!work_tex.resize(new_width, new_height, res_params))
         {
            console::error("Failed resizing texture!");
            return false;
         }
      }

      if ((generate_new_mips) && (generate_mipmaps))
      {
         bool srgb = mipmap_params.m_gamma_filtering != 0;

         const char* pFilter = crn_get_mip_filter_name(mipmap_params.m_filter);

         mipmapped_texture::generate_mipmap_params gen_params;
         gen_params.m_pFilter = pFilter;
         gen_params.m_wrapping = mipmap_params.m_tiled != 0;
         gen_params.m_renormalize = mipmap_params.m_renormalize != 0;
         gen_params.m_filter_scale = mipmap_params.m_blurriness;
         gen_params.m_gamma = mipmap_params.m_gamma;
         gen_params.m_srgb = srgb;
         gen_params.m_multithreaded = params.m_num_helper_threads > 0;
         gen_params.m_max_mips = mipmap_params.m_max_levels;
         gen_params.m_min_mip_size = mipmap_params.m_min_mip_size;

         console::info("Generating mipmaps using filter \"%s\"", pFilter);

         timer tm;
         tm.start();
         if (!work_tex.generate_mipmaps(gen_params, true))
         {
            console::error("Failed generating mipmaps!");
            return false;
         }
         double t = tm.get_elapsed_secs();

         console::info("Generated %u mipmap levels in %3.3fs", work_tex.get_num_levels() - 1, t);
      }

      return true;
   }

   bool create_compressed_texture(const crn_comp_params &params, const crn_mipmap_params &mipmap_params, crnlib::vector<uint8> &comp_data, uint32 *pActual_quality_level, float *pActual_bitrate)
   {
      comp_data.resize(0);
      if (pActual_bitrate) *pActual_bitrate = 0.0f;
      if (pActual_quality_level) *pActual_quality_level = 0;

      mipmapped_texture work_tex;
      if (!create_dds_tex(params, work_tex))
      {
         console::error("Failed creating DDS texture from crn_comp_params!");
         return false;
      }

      if (!create_texture_mipmaps(work_tex, params, mipmap_params, true))
         return false;

      crn_comp_params new_params(params);
      new_params.m_levels = work_tex.get_num_levels();
      memset(new_params.m_pImages, 0, sizeof(new_params.m_pImages));

      for (uint f = 0; f < work_tex.get_num_faces(); f++)
         for (uint l = 0; l < work_tex.get_num_levels(); l++)
            new_params.m_pImages[f][l] = (uint32*)work_tex.get_level(f, l)->get_image()->get_ptr();

      return create_compressed_texture(new_params, comp_data, pActual_quality_level, pActual_bitrate);
   }

} // namespace crnlib