// 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 <cmath>
#include <optional>

#include "fml/logging.h"
#include "impeller/core/formats.h"
#include "impeller/core/vertex_buffer.h"
#include "impeller/entity/contents/clip_contents.h"
#include "impeller/entity/contents/content_context.h"
#include "impeller/entity/contents/pipelines.h"
#include "impeller/entity/entity.h"
#include "impeller/renderer/render_pass.h"
#include "impeller/renderer/vertex_buffer_builder.h"

namespace impeller {

static Scalar GetShaderClipDepth(uint32_t clip_depth) {
  // Draw the clip at the max of the clip entity's depth slice, so that other
  // draw calls with this same depth value will be culled even if they have a
  // perspective transform.
  return std::nextafterf(Entity::GetShaderClipDepth(clip_depth + 1), 0.0f);
}

/*******************************************************************************
 ******* ClipContents
 ******************************************************************************/

ClipContents::ClipContents(Rect coverage_rect, bool is_axis_aligned_rect)
    : coverage_rect_(coverage_rect),
      is_axis_aligned_rect_(is_axis_aligned_rect) {}

ClipContents::~ClipContents() = default;

void ClipContents::SetGeometry(GeometryResult clip_geometry) {
  clip_geometry_ = std::move(clip_geometry);
}

void ClipContents::SetClipOperation(Entity::ClipOperation clip_op) {
  clip_op_ = clip_op;
}

ClipCoverage ClipContents::GetClipCoverage(
    const std::optional<Rect>& current_clip_coverage) const {
  if (!current_clip_coverage.has_value()) {
    return ClipCoverage{.coverage = std::nullopt};
  }
  switch (clip_op_) {
    case Entity::ClipOperation::kDifference:
      // This can be optimized further by considering cases when the bounds of
      // the current stencil will shrink.
      return {
          .is_difference_or_non_square = true,  //
          .coverage = current_clip_coverage     //
      };
    case Entity::ClipOperation::kIntersect:
      if (coverage_rect_.IsEmpty() || !current_clip_coverage.has_value()) {
        return ClipCoverage{.coverage = std::nullopt};
      }
      return {
          .is_difference_or_non_square = !is_axis_aligned_rect_,            //
          .coverage = current_clip_coverage->Intersection(coverage_rect_),  //
      };
  }
  FML_UNREACHABLE();
}

bool ClipContents::Render(const ContentContext& renderer,
                          RenderPass& pass,
                          uint32_t clip_depth) const {
  if (!clip_geometry_.vertex_buffer) {
    return true;
  }

  using VS = ClipPipeline::VertexShader;

  VS::FrameInfo info;
  info.depth = GetShaderClipDepth(clip_depth);

  auto options = OptionsFromPass(pass);
  options.blend_mode = BlendMode::kDst;

  pass.SetStencilReference(0);

  /// Stencil preparation draw.

  options.depth_write_enabled = false;
  options.primitive_type = clip_geometry_.type;
  pass.SetVertexBuffer(clip_geometry_.vertex_buffer);
  switch (clip_geometry_.mode) {
    case GeometryResult::Mode::kNonZero:
      pass.SetCommandLabel("Clip stencil preparation (NonZero)");
      options.stencil_mode =
          ContentContextOptions::StencilMode::kStencilNonZeroFill;
      break;
    case GeometryResult::Mode::kEvenOdd:
      pass.SetCommandLabel("Clip stencil preparation (EvenOdd)");
      options.stencil_mode =
          ContentContextOptions::StencilMode::kStencilEvenOddFill;
      break;
    case GeometryResult::Mode::kNormal:
    case GeometryResult::Mode::kPreventOverdraw:
      pass.SetCommandLabel("Clip stencil preparation (Increment)");
      options.stencil_mode =
          ContentContextOptions::StencilMode::kOverdrawPreventionIncrement;
      break;
  }
  pass.SetPipeline(renderer.GetClipPipeline(options));

  info.mvp = clip_geometry_.transform;
  VS::BindFrameInfo(pass, renderer.GetTransientsBuffer().EmplaceUniform(info));

  if (!pass.Draw().ok()) {
    return false;
  }

  /// Write depth.

  options.depth_write_enabled = true;
  options.primitive_type = PrimitiveType::kTriangleStrip;
  Rect cover_area;
  switch (clip_op_) {
    case Entity::ClipOperation::kIntersect:
      pass.SetCommandLabel("Intersect Clip");
      options.stencil_mode =
          ContentContextOptions::StencilMode::kCoverCompareInverted;
      cover_area = Rect::MakeSize(pass.GetRenderTargetSize());
      break;
    case Entity::ClipOperation::kDifference:
      pass.SetCommandLabel("Difference Clip");
      options.stencil_mode = ContentContextOptions::StencilMode::kCoverCompare;
      cover_area = coverage_rect_;
      break;
  }
  auto points = cover_area.GetPoints();
  pass.SetVertexBuffer(
      CreateVertexBuffer(points, renderer.GetTransientsBuffer()));

  pass.SetPipeline(renderer.GetClipPipeline(options));

  info.mvp = pass.GetOrthographicTransform();
  VS::BindFrameInfo(pass, renderer.GetTransientsBuffer().EmplaceUniform(info));

  return pass.Draw().ok();
}

/*******************************************************************************
 ******* ClipRestoreContents
 ******************************************************************************/

bool RenderClipRestore(const ContentContext& renderer,
                       RenderPass& pass,
                       uint32_t clip_depth,
                       std::optional<Rect> restore_coverage) {
  using VS = ClipPipeline::VertexShader;

  pass.SetCommandLabel("Restore Clip");
  auto options = OptionsFromPass(pass);
  options.blend_mode = BlendMode::kDst;
  options.stencil_mode =
      ContentContextOptions::StencilMode::kOverdrawPreventionRestore;
  options.primitive_type = PrimitiveType::kTriangleStrip;
  pass.SetPipeline(renderer.GetClipPipeline(options));
  pass.SetStencilReference(0);

  // Create a rect that covers either the given restore area, or the whole
  // render target texture.
  auto ltrb =
      restore_coverage.value_or(Rect::MakeSize(pass.GetRenderTargetSize()))
          .GetLTRB();

  std::array<VS::PerVertexData, 4> vertices = {
      VS::PerVertexData{Point(ltrb[0], ltrb[1])},
      VS::PerVertexData{Point(ltrb[2], ltrb[1])},
      VS::PerVertexData{Point(ltrb[0], ltrb[3])},
      VS::PerVertexData{Point(ltrb[2], ltrb[3])},
  };
  pass.SetVertexBuffer(
      CreateVertexBuffer(vertices, renderer.GetTransientsBuffer()));

  VS::FrameInfo info;
  info.depth = GetShaderClipDepth(clip_depth);
  info.mvp = pass.GetOrthographicTransform();
  VS::BindFrameInfo(pass, renderer.GetTransientsBuffer().EmplaceUniform(info));

  return pass.Draw().ok();
}

}  // namespace impeller
