// 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/line_contents.h"
#include "impeller/entity/contents/clip_contents.h"
#include "impeller/entity/contents/color_source_contents.h"
#include "impeller/entity/geometry/rect_geometry.h"
#include "impeller/renderer/texture_util.h"

namespace impeller {

using VS = LinePipeline::VertexShader;
using FS = LinePipeline::FragmentShader;

namespace {
using BindFragmentCallback = std::function<bool(RenderPass& pass)>;
using PipelineBuilderCallback =
    std::function<PipelineRef(ContentContextOptions)>;
using CreateGeometryCallback =
    std::function<GeometryResult(const ContentContext& renderer,
                                 const Entity& entity,
                                 RenderPass& pass,
                                 const Geometry* geometry)>;

const int32_t kCurveResolution = 32;

uint8_t DoubleToUint8(double x) {
  return static_cast<uint8_t>(std::clamp(std::round(x * 255.0), 0.0, 255.0));
}

/// See also: CreateGradientTexture
std::shared_ptr<Texture> CreateCurveTexture(
    Scalar width,
    Scalar radius,
    Scalar scale,
    const std::shared_ptr<impeller::Context>& context) {
  //
  impeller::TextureDescriptor texture_descriptor;
  texture_descriptor.storage_mode = impeller::StorageMode::kHostVisible;
  texture_descriptor.format = PixelFormat::kR8UNormInt;
  texture_descriptor.size = {kCurveResolution, 1};

  std::vector<uint8_t> curve_data =
      LineContents::CreateCurveData(width, radius, scale);

  return CreateTexture(texture_descriptor, curve_data, context, "LineCurve");
}

std::pair<LineContents::EffectiveLineParameters, GeometryResult> CreateGeometry(
    const ContentContext& renderer,
    const Entity& entity,
    RenderPass& pass,
    const Geometry* geometry) {
  using PerVertexData = LineVertexShader::PerVertexData;
  const LineGeometry* line_geometry =
      static_cast<const LineGeometry*>(geometry);

  auto& transform = entity.GetTransform();
  auto& data_host_buffer = renderer.GetTransientsDataBuffer();

  size_t count = 4;
  fml::StatusOr<LineContents::EffectiveLineParameters> calculate_status =
      LineContents::EffectiveLineParameters{.width = 0, .radius = 0};
  BufferView vertex_buffer = data_host_buffer.Emplace(
      count * sizeof(PerVertexData), alignof(PerVertexData),
      [line_geometry, &transform, &calculate_status](uint8_t* buffer) {
        auto vertices = reinterpret_cast<PerVertexData*>(buffer);
        calculate_status = LineContents::CalculatePerVertex(
            vertices, line_geometry, transform);
      });
  if (!calculate_status.ok()) {
    return std::make_pair(
        LineContents::EffectiveLineParameters{
            .width = line_geometry->GetWidth(),
            .radius = LineContents::kSampleRadius},
        kEmptyResult);
  }

  // We do the math in CalculatePerVertex in unrotated space.  This then applies
  // the rotation to the line.
  Point diff = line_geometry->GetP1() - line_geometry->GetP0();
  Scalar angle = std::atan2(diff.y, diff.x);
  Entity rotated_entity = entity.Clone();
  Matrix matrix = entity.GetTransform();
  matrix = matrix * Matrix::MakeTranslation(line_geometry->GetP0()) *
           Matrix::MakeRotationZ(Radians(angle)) *
           Matrix::MakeTranslation(-1 * line_geometry->GetP0());
  rotated_entity.SetTransform(matrix);

  return std::make_pair(
      calculate_status.value(),
      GeometryResult{
          .type = PrimitiveType::kTriangleStrip,
          .vertex_buffer =
              {
                  .vertex_buffer = vertex_buffer,
                  .vertex_count = count,
                  .index_type = IndexType::kNone,
              },
          .transform = rotated_entity.GetShaderTransform(pass),
      });
}

struct LineInfo {
  Vector3 e0;
  Vector3 e1;
  Vector3 e2;
  Vector3 e3;
};

LineInfo CalculateLineInfo(Point p0, Point p1, Scalar width, Scalar radius) {
  Vector2 diff = p0 - p1;
  float k = 2.0 / ((2.0 * radius + width) * sqrt(diff.Dot(diff)));

  return LineInfo{
      .e0 = Vector3(k * (p0.y - p1.y),  //
                    k * (p1.x - p0.x),  //
                    1.0 + k * (p0.x * p1.y - p1.x * p0.y)),
      .e1 = Vector3(
          k * (p1.x - p0.x),  //
          k * (p1.y - p0.y),  //
          1.0 + k * (p0.x * p0.x + p0.y * p0.y - p0.x * p1.x - p0.y * p1.y)),
      .e2 = Vector3(k * (p1.y - p0.y),  //
                    k * (p0.x - p1.x),  //
                    1.0 + k * (p1.x * p0.y - p0.x * p1.y)),
      .e3 = Vector3(
          k * (p0.x - p1.x),  //
          k * (p0.y - p1.y),  //
          1.0 + k * (p1.x * p1.x + p1.y * p1.y - p0.x * p1.x - p0.y * p1.y)),
  };
}
}  // namespace

const Scalar LineContents::kSampleRadius = 1.f;

std::unique_ptr<LineContents> LineContents::Make(
    std::unique_ptr<LineGeometry> geometry,
    Color color) {
  return std::unique_ptr<LineContents>(
      new LineContents(std::move(geometry), color));
}

LineContents::LineContents(std::unique_ptr<LineGeometry> geometry, Color color)
    : geometry_(std::move(geometry)), color_(color) {}

bool LineContents::Render(const ContentContext& renderer,
                          const Entity& entity,
                          RenderPass& pass) const {
  auto& data_host_buffer = renderer.GetTransientsDataBuffer();

  VS::FrameInfo frame_info;
  FS::FragInfo frag_info;
  frag_info.color = color_;

  Scalar scale = entity.GetTransform().GetMaxBasisLengthXY();

  auto geometry_result =
      CreateGeometry(renderer, entity, pass, geometry_.get());

  std::shared_ptr<Texture> curve_texture = CreateCurveTexture(
      geometry_->GetWidth(), kSampleRadius, scale, renderer.GetContext());

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

  FS::BindCurve(
      pass, curve_texture,
      renderer.GetContext()->GetSamplerLibrary()->GetSampler(sampler_desc));

  PipelineBuilderCallback pipeline_callback =
      [&renderer](ContentContextOptions options) {
        return renderer.GetLinePipeline(options);
      };

  return ColorSourceContents::DrawGeometry<VS>(
      this, geometry_.get(), renderer, entity, pass, pipeline_callback,
      frame_info,
      /*bind_fragment_callback=*/
      [&frag_info, &data_host_buffer](RenderPass& pass) {
        FS::BindFragInfo(pass, data_host_buffer.EmplaceUniform(frag_info));
        pass.SetCommandLabel("Line");
        return true;
      },
      /*force_stencil=*/false,
      /*create_geom_callback=*/
      [geometry_result = std::move(geometry_result)](
          const ContentContext& renderer, const Entity& entity,
          RenderPass& pass,
          const Geometry* geometry) { return geometry_result.second; });
}

std::optional<Rect> LineContents::GetCoverage(const Entity& entity) const {
  return geometry_->GetCoverage(entity.GetTransform());
}

std::vector<uint8_t> LineContents::CreateCurveData(Scalar width,
                                                   Scalar radius,
                                                   Scalar scale) {
  std::vector<uint8_t> curve_data;
  curve_data.reserve(kCurveResolution);
  // More simply written as rise / run:
  // double slope = 1.0 / ((radius * 2) / (scale * width + radius));
  double slope = (scale * width + radius) / (radius * 2);
  for (int i = 0; i < kCurveResolution; ++i) {
    double norm =
        (static_cast<double>(i)) / static_cast<double>(kCurveResolution - 1);
    double scaled = slope * norm;
    curve_data.push_back(DoubleToUint8(scaled));
  }
  return curve_data;
}

namespace {
void ExpandLine(std::array<Point, 4>& corners, Point expansion) {
  Point along = (corners[1] - corners[0]).Normalize();
  Point across = (corners[2] - corners[0]).Normalize();
  corners[0] += -1 * (across * expansion.x) + -1 * (along * expansion.y);
  corners[1] += -1 * (across * expansion.x) + (along * expansion.y);
  corners[2] += (across * expansion.x) + -1 * (along * expansion.y);
  corners[3] += (across * expansion.x) + (along * expansion.y);
}
}  // namespace

fml::StatusOr<LineContents::EffectiveLineParameters>
LineContents::CalculatePerVertex(LineVertexShader::PerVertexData* per_vertex,
                                 const LineGeometry* geometry,
                                 const Matrix& entity_transform) {
  Scalar scale = entity_transform.GetMaxBasisLengthXY();

  // Transform the line into unrotated space by rotating p1 to be horizontal
  // with p0. We do this because there seems to be a flaw in the eN calculations
  // where they create thinner lines for diagonal lines.
  Point diff = geometry->GetP1() - geometry->GetP0();
  Scalar magnitude = diff.GetLength();
  Point p1_prime = Point(geometry->GetP0().x + magnitude, geometry->GetP0().y);

  std::array<Point, 4> corners;
  // Make sure we get kSampleRadius pixels to sample from.
  Scalar expand_size = std::max(kSampleRadius / scale, kSampleRadius);
  if (!LineGeometry::ComputeCorners(
          corners.data(), entity_transform,
          /*extend_endpoints=*/geometry->GetCap() != Cap::kButt,
          geometry->GetP0(), p1_prime, geometry->GetWidth())) {
    return fml::Status(fml::StatusCode::kAborted, "No valid corners");
  }
  Scalar effective_line_width = std::fabsf((corners[2] - corners[0]).y);
  ExpandLine(corners, Point(expand_size, expand_size));
  Scalar padded_line_width = std::fabsf((corners[2] - corners[0]).y);
  Scalar effective_sample_radius =
      (padded_line_width - effective_line_width) / 2.f;
  LineInfo line_info =
      CalculateLineInfo(geometry->GetP0(), p1_prime, effective_line_width,
                        effective_sample_radius);
  for (auto& corner : corners) {
    *per_vertex++ = {
        .position = corner,
        .e0 = line_info.e0,
        .e1 = line_info.e1,
        .e2 = line_info.e2,
        .e3 = line_info.e3,
    };
  }

  return EffectiveLineParameters{.width = effective_line_width,
                                 .radius = effective_sample_radius};
}
}  // namespace impeller
