// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "impeller/entity/contents/filters/gaussian_blur_filter_contents.h"

#include <cmath>
#include <utility>
#include <valarray>

#include "impeller/base/strings.h"
#include "impeller/base/validation.h"
#include "impeller/core/formats.h"
#include "impeller/core/sampler_descriptor.h"
#include "impeller/entity/contents/content_context.h"
#include "impeller/entity/contents/filters/filter_contents.h"
#include "impeller/geometry/rect.h"
#include "impeller/geometry/scalar.h"
#include "impeller/renderer/command_buffer.h"
#include "impeller/renderer/render_pass.h"
#include "impeller/renderer/render_target.h"
#include "impeller/renderer/sampler_library.h"

namespace impeller {

DirectionalGaussianBlurFilterContents::DirectionalGaussianBlurFilterContents() =
    default;

DirectionalGaussianBlurFilterContents::
    ~DirectionalGaussianBlurFilterContents() = default;

void DirectionalGaussianBlurFilterContents::SetSigma(Sigma sigma) {
  blur_sigma_ = sigma;
}

void DirectionalGaussianBlurFilterContents::SetSecondarySigma(Sigma sigma) {
  secondary_blur_sigma_ = sigma;
}

void DirectionalGaussianBlurFilterContents::SetDirection(Vector2 direction) {
  blur_direction_ = direction.Normalize();
  if (blur_direction_.IsZero()) {
    blur_direction_ = Vector2(0, 1);
  }
}

void DirectionalGaussianBlurFilterContents::SetBlurStyle(BlurStyle blur_style) {
  blur_style_ = blur_style;

  switch (blur_style) {
    case FilterContents::BlurStyle::kNormal:
      src_color_factor_ = false;
      inner_blur_factor_ = true;
      outer_blur_factor_ = true;
      break;
    case FilterContents::BlurStyle::kSolid:
      src_color_factor_ = true;
      inner_blur_factor_ = false;
      outer_blur_factor_ = true;
      break;
    case FilterContents::BlurStyle::kOuter:
      src_color_factor_ = false;
      inner_blur_factor_ = false;
      outer_blur_factor_ = true;
      break;
    case FilterContents::BlurStyle::kInner:
      src_color_factor_ = false;
      inner_blur_factor_ = true;
      outer_blur_factor_ = false;
      break;
  }
}

void DirectionalGaussianBlurFilterContents::SetTileMode(
    Entity::TileMode tile_mode) {
  tile_mode_ = tile_mode;
}

void DirectionalGaussianBlurFilterContents::SetSourceOverride(
    FilterInput::Ref source_override) {
  source_override_ = std::move(source_override);
}

std::optional<Entity> DirectionalGaussianBlurFilterContents::RenderFilter(
    const FilterInput::Vector& inputs,
    const ContentContext& renderer,
    const Entity& entity,
    const Matrix& effect_transform,
    const Rect& coverage,
    const std::optional<Rect>& coverage_hint) const {
  using VS = GaussianBlurAlphaDecalPipeline::VertexShader;
  using FS = GaussianBlurAlphaDecalPipeline::FragmentShader;

  bool is_first_pass = !source_override_;

  //----------------------------------------------------------------------------
  /// Handle inputs.
  ///

  if (inputs.empty()) {
    return std::nullopt;
  }

  // Limit the kernel size to 1000x1000 pixels, like Skia does.
  auto radius = std::min(Radius{blur_sigma_}.radius, 500.0f);

  // Input 0 snapshot.

  std::optional<Rect> expanded_coverage_hint;
  if (coverage_hint.has_value()) {
    auto r = Size(radius, radius).Abs();
    expanded_coverage_hint =
        is_first_pass ? Rect(coverage_hint.value().origin - r,
                             Size(coverage_hint.value().size + r * 2))
                      : coverage_hint;
  }
  auto input_snapshot = inputs[0]->GetSnapshot("GaussianBlur", renderer, entity,
                                               expanded_coverage_hint);
  if (!input_snapshot.has_value()) {
    return std::nullopt;
  }

  if (blur_sigma_.sigma < kEhCloseEnough) {
    return Entity::FromSnapshot(
        input_snapshot.value(), entity.GetBlendMode(),
        entity.GetStencilDepth());  // No blur to render.
  }

  auto transform = entity.GetTransformation() * effect_transform.Basis();
  auto transformed_blur_radius =
      transform.TransformDirection(blur_direction_ * radius);

  auto transformed_blur_radius_length = transformed_blur_radius.GetLength();

  // If the radius length is < .5, the shader will take at most 1 sample,
  // resulting in no blur.
  if (transformed_blur_radius_length < .5) {
    return Entity::FromSnapshot(
        input_snapshot.value(), entity.GetBlendMode(),
        entity.GetStencilDepth());  // No blur to render.
  }

  // A matrix that rotates the snapshot space such that the blur direction is
  // +X.
  auto texture_rotate = Matrix::MakeRotationZ(
      transformed_blur_radius.Normalize().AngleTo({1, 0}));

  // Converts local pass space to screen space. This is just the snapshot space
  // rotated such that the blur direction is +X.
  auto pass_transform = texture_rotate * input_snapshot->transform;

  // The pass texture coverage, but rotated such that the blur is in the +X
  // direction, and expanded to include the blur radius. This is used for UV
  // projection and as a source for the pass size. Note that it doesn't matter
  // which direction the space is rotated in when grabbing the pass size.
  auto pass_texture_rect = Rect::MakeSize(input_snapshot->texture->GetSize())
                               .TransformBounds(pass_transform);
  pass_texture_rect.origin.x -= transformed_blur_radius_length;
  pass_texture_rect.size.width += transformed_blur_radius_length * 2;

  // Crop the pass texture with the rotated coverage hint if one was given.
  if (expanded_coverage_hint.has_value()) {
    auto maybe_pass_texture_rect = pass_texture_rect.Intersection(
        expanded_coverage_hint->TransformBounds(texture_rotate));
    if (!maybe_pass_texture_rect.has_value()) {
      return std::nullopt;
    }
    pass_texture_rect = *maybe_pass_texture_rect;
  }

  // Source override snapshot.

  auto source = source_override_ ? source_override_ : inputs[0];
  auto source_snapshot = source->GetSnapshot("GaussianBlur(Override)", renderer,
                                             entity, GetCoverageHint());
  if (!source_snapshot.has_value()) {
    return std::nullopt;
  }

  // UV mapping.

  auto pass_uv_project = [&texture_rotate,
                          &pass_texture_rect](Snapshot& input) {
    auto uv_matrix = Matrix::MakeScale(1 / Vector2(input.texture->GetSize())) *
                     (texture_rotate * input.transform).Invert();
    return pass_texture_rect.GetTransformedPoints(uv_matrix);
  };

  auto input_uvs = pass_uv_project(input_snapshot.value());

  auto source_uvs = pass_uv_project(source_snapshot.value());

  //----------------------------------------------------------------------------
  /// Render to texture.
  ///

  ContentContext::SubpassCallback subpass_callback = [&](const ContentContext&
                                                             renderer,
                                                         RenderPass& pass) {
    auto& host_buffer = pass.GetTransientsBuffer();

    VertexBufferBuilder<VS::PerVertexData> vtx_builder;
    vtx_builder.AddVertices({
        {Point(0, 0), input_uvs[0], source_uvs[0]},
        {Point(1, 0), input_uvs[1], source_uvs[1]},
        {Point(1, 1), input_uvs[3], source_uvs[3]},
        {Point(0, 0), input_uvs[0], source_uvs[0]},
        {Point(1, 1), input_uvs[3], source_uvs[3]},
        {Point(0, 1), input_uvs[2], source_uvs[2]},
    });
    auto vtx_buffer = vtx_builder.CreateVertexBuffer(host_buffer);

    VS::FrameInfo frame_info;
    frame_info.mvp = Matrix::MakeOrthographic(ISize(1, 1));
    frame_info.texture_sampler_y_coord_scale =
        input_snapshot->texture->GetYCoordScale();
    frame_info.alpha_mask_sampler_y_coord_scale =
        source_snapshot->texture->GetYCoordScale();

    FS::BlurInfo frag_info;
    auto r = Radius{transformed_blur_radius_length};
    frag_info.blur_sigma = Sigma{r}.sigma;
    frag_info.blur_radius = std::round(r.radius);

    // The blur direction is in input UV space.
    frag_info.blur_uv_offset =
        pass_transform.Invert().TransformDirection(Vector2(1, 0)).Normalize() /
        Point(input_snapshot->GetCoverage().value().size);

    Command cmd;
    DEBUG_COMMAND_INFO(cmd, SPrintF("Gaussian Blur Filter (Radius=%.2f)",
                                    transformed_blur_radius_length));
    cmd.BindVertices(vtx_buffer);

    auto options = OptionsFromPass(pass);
    options.blend_mode = BlendMode::kSource;
    auto input_descriptor = input_snapshot->sampler_descriptor;
    auto source_descriptor = source_snapshot->sampler_descriptor;
    switch (tile_mode_) {
      case Entity::TileMode::kDecal:
        if (renderer.GetDeviceCapabilities().SupportsDecalTileMode()) {
          input_descriptor.width_address_mode = SamplerAddressMode::kDecal;
          input_descriptor.height_address_mode = SamplerAddressMode::kDecal;
          source_descriptor.width_address_mode = SamplerAddressMode::kDecal;
          source_descriptor.height_address_mode = SamplerAddressMode::kDecal;
        }
        break;
      case Entity::TileMode::kClamp:
        input_descriptor.width_address_mode = SamplerAddressMode::kClampToEdge;
        input_descriptor.height_address_mode = SamplerAddressMode::kClampToEdge;
        source_descriptor.width_address_mode = SamplerAddressMode::kClampToEdge;
        source_descriptor.height_address_mode =
            SamplerAddressMode::kClampToEdge;
        break;
      case Entity::TileMode::kMirror:
        input_descriptor.width_address_mode = SamplerAddressMode::kMirror;
        input_descriptor.height_address_mode = SamplerAddressMode::kMirror;
        source_descriptor.width_address_mode = SamplerAddressMode::kMirror;
        source_descriptor.height_address_mode = SamplerAddressMode::kMirror;
        break;
      case Entity::TileMode::kRepeat:
        input_descriptor.width_address_mode = SamplerAddressMode::kRepeat;
        input_descriptor.height_address_mode = SamplerAddressMode::kRepeat;
        source_descriptor.width_address_mode = SamplerAddressMode::kRepeat;
        source_descriptor.height_address_mode = SamplerAddressMode::kRepeat;
        break;
    }
    input_descriptor.mag_filter = MinMagFilter::kLinear;
    input_descriptor.min_filter = MinMagFilter::kLinear;

    bool has_alpha_mask = blur_style_ != BlurStyle::kNormal;
    bool has_decal_specialization =
        tile_mode_ == Entity::TileMode::kDecal &&
        !renderer.GetDeviceCapabilities().SupportsDecalTileMode();

    if (has_alpha_mask && has_decal_specialization) {
      cmd.pipeline = renderer.GetGaussianBlurAlphaDecalPipeline(options);
    } else if (has_alpha_mask) {
      cmd.pipeline = renderer.GetGaussianBlurAlphaPipeline(options);
    } else if (has_decal_specialization) {
      cmd.pipeline = renderer.GetGaussianBlurDecalPipeline(options);
    } else {
      cmd.pipeline = renderer.GetGaussianBlurPipeline(options);
    }

    FS::BindTextureSampler(
        cmd, input_snapshot->texture,
        renderer.GetContext()->GetSamplerLibrary()->GetSampler(
            input_descriptor));
    VS::BindFrameInfo(cmd, host_buffer.EmplaceUniform(frame_info));
    FS::BindBlurInfo(cmd, host_buffer.EmplaceUniform(frag_info));

    if (has_alpha_mask) {
      FS::MaskInfo mask_info;
      mask_info.src_factor = src_color_factor_;
      mask_info.inner_blur_factor = inner_blur_factor_;
      mask_info.outer_blur_factor = outer_blur_factor_;

      FS::BindAlphaMaskSampler(
          cmd, source_snapshot->texture,
          renderer.GetContext()->GetSamplerLibrary()->GetSampler(
              source_descriptor));
      FS::BindMaskInfo(cmd, host_buffer.EmplaceUniform(mask_info));
    }

    return pass.AddCommand(std::move(cmd));
  };

  Vector2 scale;
  auto scale_curve = [](Scalar radius) {
    constexpr Scalar decay = 4.0;   // Larger is more gradual.
    constexpr Scalar limit = 0.95;  // The maximum percentage of the scaledown.
    const Scalar curve =
        std::min(1.0, decay / (std::max(1.0f, radius) + decay - 1.0));
    return (curve - 1) * limit + 1;
  };
  {
    scale.x = scale_curve(transformed_blur_radius_length);

    Scalar y_radius = std::abs(pass_transform.GetDirectionScale(
        Vector2(0, !is_first_pass ? Radius{secondary_blur_sigma_}.radius : 1)));
    scale.y = scale_curve(y_radius);
  }

  Vector2 scaled_size = pass_texture_rect.size * scale;
  ISize floored_size = ISize(scaled_size.x, scaled_size.y);

  auto out_texture = renderer.MakeSubpass("Directional Gaussian Blur Filter",
                                          floored_size, subpass_callback);

  if (!out_texture) {
    return std::nullopt;
  }

  SamplerDescriptor sampler_desc;
  sampler_desc.min_filter = MinMagFilter::kLinear;
  sampler_desc.mag_filter = MinMagFilter::kLinear;

  return Entity::FromSnapshot(
      Snapshot{.texture = out_texture,
               .transform = texture_rotate.Invert() *
                            Matrix::MakeTranslation(pass_texture_rect.origin) *
                            Matrix::MakeScale((1 / scale) *
                                              (scaled_size / floored_size)),
               .sampler_descriptor = sampler_desc,
               .opacity = input_snapshot->opacity},
      entity.GetBlendMode(), entity.GetStencilDepth());
}

std::optional<Rect> DirectionalGaussianBlurFilterContents::GetFilterCoverage(
    const FilterInput::Vector& inputs,
    const Entity& entity,
    const Matrix& effect_transform) const {
  if (inputs.empty()) {
    return std::nullopt;
  }

  auto coverage = inputs[0]->GetCoverage(entity);
  if (!coverage.has_value()) {
    return std::nullopt;
  }

  auto transform = inputs[0]->GetTransform(entity) * effect_transform.Basis();
  auto transformed_blur_vector =
      transform.TransformDirection(blur_direction_ * Radius{blur_sigma_}.radius)
          .Abs();
  auto extent = coverage->size + transformed_blur_vector * 2;
  return Rect(coverage->origin - transformed_blur_vector,
              Size(extent.x, extent.y));
}

}  // namespace impeller
