/*
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
 *
 * Copyright 2021, Blender Foundation.
 */

#include "COM_GaussianAlphaBlurBaseOperation.h"

namespace blender::compositor {

GaussianAlphaBlurBaseOperation::GaussianAlphaBlurBaseOperation(eDimension dim)
    : BlurBaseOperation(DataType::Value)
{
  gausstab_ = nullptr;
  filtersize_ = 0;
  falloff_ = -1; /* Intentionally invalid, so we can detect uninitialized values. */
  dimension_ = dim;
}

void GaussianAlphaBlurBaseOperation::init_data()
{
  BlurBaseOperation::init_data();
  if (execution_model_ == eExecutionModel::FullFrame) {
    rad_ = max_ff(size_ * this->get_blur_size(dimension_), 0.0f);
    rad_ = min_ff(rad_, MAX_GAUSSTAB_RADIUS);
    filtersize_ = min_ii(ceil(rad_), MAX_GAUSSTAB_RADIUS);
  }
}

void GaussianAlphaBlurBaseOperation::init_execution()
{
  BlurBaseOperation::init_execution();
  if (execution_model_ == eExecutionModel::FullFrame) {
    gausstab_ = BlurBaseOperation::make_gausstab(rad_, filtersize_);
    distbuf_inv_ = BlurBaseOperation::make_dist_fac_inverse(rad_, filtersize_, falloff_);
  }
}

void GaussianAlphaBlurBaseOperation::deinit_execution()
{
  BlurBaseOperation::deinit_execution();

  if (gausstab_) {
    MEM_freeN(gausstab_);
    gausstab_ = nullptr;
  }

  if (distbuf_inv_) {
    MEM_freeN(distbuf_inv_);
    distbuf_inv_ = nullptr;
  }
}

void GaussianAlphaBlurBaseOperation::get_area_of_interest(const int input_idx,
                                                          const rcti &output_area,
                                                          rcti &r_input_area)
{
  if (input_idx != IMAGE_INPUT_INDEX) {
    BlurBaseOperation::get_area_of_interest(input_idx, output_area, r_input_area);
    return;
  }

  r_input_area = output_area;
  switch (dimension_) {
    case eDimension::X:
      r_input_area.xmin = output_area.xmin - filtersize_ - 1;
      r_input_area.xmax = output_area.xmax + filtersize_ + 1;
      break;
    case eDimension::Y:
      r_input_area.ymin = output_area.ymin - filtersize_ - 1;
      r_input_area.ymax = output_area.ymax + filtersize_ + 1;
      break;
  }
}

BLI_INLINE float finv_test(const float f, const bool test)
{
  return (LIKELY(test == false)) ? f : 1.0f - f;
}

void GaussianAlphaBlurBaseOperation::update_memory_buffer_partial(MemoryBuffer *output,
                                                                  const rcti &area,
                                                                  Span<MemoryBuffer *> inputs)
{
  MemoryBuffer *input = inputs[IMAGE_INPUT_INDEX];
  const rcti &input_rect = input->get_rect();
  BuffersIterator<float> it = output->iterate_with({input}, area);

  int min_input_coord = -1;
  int max_input_coord = -1;
  int elem_stride = -1;
  std::function<int()> get_current_coord;
  switch (dimension_) {
    case eDimension::X:
      min_input_coord = input_rect.xmin;
      max_input_coord = input_rect.xmax;
      get_current_coord = [&] { return it.x; };
      elem_stride = input->elem_stride;
      break;
    case eDimension::Y:
      min_input_coord = input_rect.ymin;
      max_input_coord = input_rect.ymax;
      get_current_coord = [&] { return it.y; };
      elem_stride = input->row_stride;
      break;
  }

  for (; !it.is_end(); ++it) {
    const int coord = get_current_coord();
    const int coord_min = max_ii(coord - filtersize_, min_input_coord);
    const int coord_max = min_ii(coord + filtersize_ + 1, max_input_coord);

    /* *** This is the main part which is different to #GaussianBlurBaseOperation.  *** */
    /* Gauss. */
    float alpha_accum = 0.0f;
    float multiplier_accum = 0.0f;

    /* Dilate. */
    const bool do_invert = do_subtract_;
    /* Init with the current color to avoid unneeded lookups. */
    float value_max = finv_test(*it.in(0), do_invert);
    float distfacinv_max = 1.0f; /* 0 to 1 */

    const int step = QualityStepHelper::get_step();
    const float *in = it.in(0) + ((intptr_t)coord_min - coord) * elem_stride;
    const int in_stride = elem_stride * step;
    int index = (coord_min - coord) + filtersize_;
    const int index_end = index + (coord_max - coord_min);
    for (; index < index_end; in += in_stride, index += step) {
      float value = finv_test(*in, do_invert);

      /* Gauss. */
      float multiplier = gausstab_[index];
      alpha_accum += value * multiplier;
      multiplier_accum += multiplier;

      /* Dilate - find most extreme color. */
      if (value > value_max) {
        multiplier = distbuf_inv_[index];
        value *= multiplier;
        if (value > value_max) {
          value_max = value;
          distfacinv_max = multiplier;
        }
      }
    }

    /* Blend between the max value and gauss blue - gives nice feather. */
    const float value_blur = alpha_accum / multiplier_accum;
    const float value_final = (value_max * distfacinv_max) +
                              (value_blur * (1.0f - distfacinv_max));
    *it.out = finv_test(value_final, do_invert);
  }
}

}  // namespace blender::compositor
