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

#include "flutter/fml/macros.h"

#include "impeller/core/formats.h"
#include "impeller/entity/contents/atlas_contents.h"
#include "impeller/entity/contents/content_context.h"
#include "impeller/entity/contents/filters/color_filter_contents.h"
#include "impeller/entity/contents/filters/filter_contents.h"
#include "impeller/entity/contents/framebuffer_blend_contents.h"
#include "impeller/entity/contents/texture_contents.h"
#include "impeller/entity/entity.h"
#include "impeller/entity/geometry/geometry.h"
#include "impeller/entity/texture_fill.frag.h"
#include "impeller/entity/texture_fill.vert.h"
#include "impeller/renderer/render_pass.h"
#include "impeller/renderer/sampler_library.h"
#include "impeller/renderer/vertex_buffer_builder.h"

namespace impeller {

AtlasContents::AtlasContents() = default;

AtlasContents::~AtlasContents() = default;

void AtlasContents::SetTexture(std::shared_ptr<Texture> texture) {
  texture_ = std::move(texture);
}

std::shared_ptr<Texture> AtlasContents::GetTexture() const {
  return texture_;
}

void AtlasContents::SetTransforms(std::vector<Matrix> transforms) {
  transforms_ = std::move(transforms);
  bounding_box_cache_.reset();
}

void AtlasContents::SetTextureCoordinates(std::vector<Rect> texture_coords) {
  texture_coords_ = std::move(texture_coords);
  bounding_box_cache_.reset();
}

void AtlasContents::SetColors(std::vector<Color> colors) {
  colors_ = std::move(colors);
}

void AtlasContents::SetAlpha(Scalar alpha) {
  alpha_ = alpha;
}

void AtlasContents::SetBlendMode(BlendMode blend_mode) {
  blend_mode_ = blend_mode;
}

void AtlasContents::SetCullRect(std::optional<Rect> cull_rect) {
  cull_rect_ = cull_rect;
}

struct AtlasBlenderKey {
  Color color;
  Rect rect;
  uint32_t color_key;

  struct Hash {
    std::size_t operator()(const AtlasBlenderKey& key) const {
      return fml::HashCombine(key.color_key, key.rect.size.width,
                              key.rect.size.height, key.rect.origin.x,
                              key.rect.origin.y);
    }
  };

  struct Equal {
    bool operator()(const AtlasBlenderKey& lhs,
                    const AtlasBlenderKey& rhs) const {
      return lhs.rect == rhs.rect && lhs.color_key == rhs.color_key;
    }
  };
};

std::shared_ptr<SubAtlasResult> AtlasContents::GenerateSubAtlas() const {
  FML_DCHECK(colors_.size() > 0 && blend_mode_ != BlendMode::kSource &&
             blend_mode_ != BlendMode::kDestination);

  std::unordered_map<AtlasBlenderKey, std::vector<Matrix>,
                     AtlasBlenderKey::Hash, AtlasBlenderKey::Equal>
      sub_atlas = {};

  for (auto i = 0u; i < texture_coords_.size(); i++) {
    AtlasBlenderKey key = {.color = colors_[i],
                           .rect = texture_coords_[i],
                           .color_key = Color::ToIColor(colors_[i])};
    if (sub_atlas.find(key) == sub_atlas.end()) {
      sub_atlas[key] = {transforms_[i]};
    } else {
      sub_atlas[key].push_back(transforms_[i]);
    }
  }

  auto result = std::make_shared<SubAtlasResult>();
  Scalar x_offset = 0.0;
  Scalar y_offset = 0.0;
  Scalar x_extent = 0.0;
  Scalar y_extent = 0.0;

  for (auto it = sub_atlas.begin(); it != sub_atlas.end(); it++) {
    // This size was arbitrarily chosen to keep the textures from getting too
    // wide. We could instead use a more generic rect packer but in the majority
    // of cases the sample rects will be fairly close in size making this a good
    // enough approximation.
    if (x_offset >= 1000) {
      y_offset = y_extent + 1;
      x_offset = 0.0;
    }

    auto key = it->first;
    auto transforms = it->second;

    auto new_rect = Rect::MakeXYWH(x_offset, y_offset, key.rect.size.width,
                                   key.rect.size.height);
    auto sub_transform = Matrix::MakeTranslation(Vector2(x_offset, y_offset));

    x_offset += std::ceil(key.rect.size.width) + 1.0;

    result->sub_texture_coords.push_back(key.rect);
    result->sub_colors.push_back(key.color);
    result->sub_transforms.push_back(sub_transform);

    x_extent = std::max(x_extent, x_offset);
    y_extent = std::max(y_extent, std::ceil(y_offset + key.rect.size.height));

    for (auto transform : transforms) {
      result->result_texture_coords.push_back(new_rect);
      result->result_transforms.push_back(transform);
    }
  }
  result->size = ISize(std::ceil(x_extent), std::ceil(y_extent));
  return result;
}

std::optional<Rect> AtlasContents::GetCoverage(const Entity& entity) const {
  if (cull_rect_.has_value()) {
    return cull_rect_.value().TransformBounds(entity.GetTransformation());
  }
  return ComputeBoundingBox().TransformBounds(entity.GetTransformation());
}

Rect AtlasContents::ComputeBoundingBox() const {
  if (!bounding_box_cache_.has_value()) {
    Rect bounding_box = {};
    for (size_t i = 0; i < texture_coords_.size(); i++) {
      auto matrix = transforms_[i];
      auto sample_rect = texture_coords_[i];
      auto bounds = Rect::MakeSize(sample_rect.size).TransformBounds(matrix);
      bounding_box = bounds.Union(bounding_box);
    }
    bounding_box_cache_ = bounding_box;
  }
  return bounding_box_cache_.value();
}

void AtlasContents::SetSamplerDescriptor(SamplerDescriptor desc) {
  sampler_descriptor_ = std::move(desc);
}

const SamplerDescriptor& AtlasContents::GetSamplerDescriptor() const {
  return sampler_descriptor_;
}

const std::vector<Matrix>& AtlasContents::GetTransforms() const {
  return transforms_;
}

const std::vector<Rect>& AtlasContents::GetTextureCoordinates() const {
  return texture_coords_;
}

const std::vector<Color>& AtlasContents::GetColors() const {
  return colors_;
}

bool AtlasContents::Render(const ContentContext& renderer,
                           const Entity& entity,
                           RenderPass& pass) const {
  if (texture_ == nullptr || blend_mode_ == BlendMode::kClear ||
      alpha_ <= 0.0) {
    return true;
  }

  // Ensure that we use the actual computed bounds and not a cull-rect
  // approximation of them.
  auto coverage = ComputeBoundingBox();

  if (blend_mode_ == BlendMode::kSource || colors_.size() == 0) {
    auto child_contents = AtlasTextureContents(*this);
    child_contents.SetAlpha(alpha_);
    child_contents.SetCoverage(coverage);
    return child_contents.Render(renderer, entity, pass);
  }
  if (blend_mode_ == BlendMode::kDestination) {
    auto child_contents = AtlasColorContents(*this);
    child_contents.SetAlpha(alpha_);
    child_contents.SetCoverage(coverage);
    return child_contents.Render(renderer, entity, pass);
  }

  auto sub_atlas = GenerateSubAtlas();
  auto sub_coverage = Rect::MakeSize(sub_atlas->size);

  auto src_contents = std::make_shared<AtlasTextureContents>(*this);
  src_contents->SetSubAtlas(sub_atlas);
  src_contents->SetCoverage(sub_coverage);

  auto dst_contents = std::make_shared<AtlasColorContents>(*this);
  dst_contents->SetSubAtlas(sub_atlas);
  dst_contents->SetCoverage(sub_coverage);

  Entity untransformed_entity;
  auto contents = ColorFilterContents::MakeBlend(
      blend_mode_,
      {FilterInput::Make(dst_contents), FilterInput::Make(src_contents)});
  auto snapshot =
      contents->RenderToSnapshot(renderer,              // renderer
                                 untransformed_entity,  // entity
                                 std::nullopt,          // coverage_limit
                                 std::nullopt,          // sampler_descriptor
                                 true,                  // msaa_enabled
                                 "AtlasContents Snapshot");  // label
  if (!snapshot.has_value()) {
    return false;
  }

  auto child_contents = AtlasTextureContents(*this);
  child_contents.SetAlpha(alpha_);
  child_contents.SetCoverage(coverage);
  child_contents.SetTexture(snapshot.value().texture);
  child_contents.SetUseDestination(true);
  child_contents.SetSubAtlas(sub_atlas);
  return child_contents.Render(renderer, entity, pass);
}

// AtlasTextureContents
// ---------------------------------------------------------

AtlasTextureContents::AtlasTextureContents(const AtlasContents& parent)
    : parent_(parent) {}

AtlasTextureContents::~AtlasTextureContents() {}

std::optional<Rect> AtlasTextureContents::GetCoverage(
    const Entity& entity) const {
  return coverage_.TransformBounds(entity.GetTransformation());
}

void AtlasTextureContents::SetAlpha(Scalar alpha) {
  alpha_ = alpha;
}

void AtlasTextureContents::SetCoverage(Rect coverage) {
  coverage_ = coverage;
}

void AtlasTextureContents::SetUseDestination(bool value) {
  use_destination_ = value;
}

void AtlasTextureContents::SetSubAtlas(
    const std::shared_ptr<SubAtlasResult>& subatlas) {
  subatlas_ = subatlas;
}

void AtlasTextureContents::SetTexture(std::shared_ptr<Texture> texture) {
  texture_ = std::move(texture);
}

bool AtlasTextureContents::Render(const ContentContext& renderer,
                                  const Entity& entity,
                                  RenderPass& pass) const {
  using VS = TextureFillVertexShader;
  using FS = TextureFillFragmentShader;

  auto texture = texture_ ? texture_ : parent_.GetTexture();
  if (texture == nullptr) {
    return true;
  }

  std::vector<Rect> texture_coords;
  std::vector<Matrix> transforms;
  if (subatlas_) {
    texture_coords = use_destination_ ? subatlas_->result_texture_coords
                                      : subatlas_->sub_texture_coords;
    transforms = use_destination_ ? subatlas_->result_transforms
                                  : subatlas_->sub_transforms;
  } else {
    texture_coords = parent_.GetTextureCoordinates();
    transforms = parent_.GetTransforms();
  }

  const Size texture_size(texture->GetSize());
  VertexBufferBuilder<VS::PerVertexData> vertex_builder;
  vertex_builder.Reserve(texture_coords.size() * 6);
  constexpr size_t indices[6] = {0, 1, 2, 1, 2, 3};
  constexpr Scalar width[6] = {0, 1, 0, 1, 0, 1};
  constexpr Scalar height[6] = {0, 0, 1, 0, 1, 1};
  for (size_t i = 0; i < texture_coords.size(); i++) {
    auto sample_rect = texture_coords[i];
    auto matrix = transforms[i];
    auto transformed_points =
        Rect::MakeSize(sample_rect.size).GetTransformedPoints(matrix);

    for (size_t j = 0; j < 6; j++) {
      VS::PerVertexData data;
      data.position = transformed_points[indices[j]];
      data.texture_coords =
          (sample_rect.origin + Point(sample_rect.size.width * width[j],
                                      sample_rect.size.height * height[j])) /
          texture_size;
      vertex_builder.AppendVertex(data);
    }
  }

  if (!vertex_builder.HasVertices()) {
    return true;
  }

  Command cmd;
  DEBUG_COMMAND_INFO(cmd, "AtlasTexture");

  auto& host_buffer = pass.GetTransientsBuffer();

  VS::FrameInfo frame_info;
  frame_info.mvp = Matrix::MakeOrthographic(pass.GetRenderTargetSize()) *
                   entity.GetTransformation();
  frame_info.texture_sampler_y_coord_scale = texture->GetYCoordScale();
  frame_info.alpha = alpha_;

  auto options = OptionsFromPassAndEntity(pass, entity);
  cmd.pipeline = renderer.GetTexturePipeline(options);
  cmd.stencil_reference = entity.GetStencilDepth();
  cmd.BindVertices(vertex_builder.CreateVertexBuffer(host_buffer));
  VS::BindFrameInfo(cmd, host_buffer.EmplaceUniform(frame_info));
  FS::BindTextureSampler(cmd, texture,
                         renderer.GetContext()->GetSamplerLibrary()->GetSampler(
                             parent_.GetSamplerDescriptor()));
  return pass.AddCommand(std::move(cmd));
}

// AtlasColorContents
// ---------------------------------------------------------

AtlasColorContents::AtlasColorContents(const AtlasContents& parent)
    : parent_(parent) {}

AtlasColorContents::~AtlasColorContents() {}

std::optional<Rect> AtlasColorContents::GetCoverage(
    const Entity& entity) const {
  return coverage_.TransformBounds(entity.GetTransformation());
}

void AtlasColorContents::SetAlpha(Scalar alpha) {
  alpha_ = alpha;
}

void AtlasColorContents::SetCoverage(Rect coverage) {
  coverage_ = coverage;
}

void AtlasColorContents::SetSubAtlas(
    const std::shared_ptr<SubAtlasResult>& subatlas) {
  subatlas_ = subatlas;
}

bool AtlasColorContents::Render(const ContentContext& renderer,
                                const Entity& entity,
                                RenderPass& pass) const {
  using VS = GeometryColorPipeline::VertexShader;
  using FS = GeometryColorPipeline::FragmentShader;

  std::vector<Rect> texture_coords;
  std::vector<Matrix> transforms;
  std::vector<Color> colors;
  if (subatlas_) {
    texture_coords = subatlas_->sub_texture_coords;
    colors = subatlas_->sub_colors;
    transforms = subatlas_->sub_transforms;
  } else {
    texture_coords = parent_.GetTextureCoordinates();
    transforms = parent_.GetTransforms();
    colors = parent_.GetColors();
  }

  VertexBufferBuilder<VS::PerVertexData> vertex_builder;
  vertex_builder.Reserve(texture_coords.size() * 6);
  constexpr size_t indices[6] = {0, 1, 2, 1, 2, 3};
  for (size_t i = 0; i < texture_coords.size(); i++) {
    auto sample_rect = texture_coords[i];
    auto matrix = transforms[i];
    auto transformed_points =
        Rect::MakeSize(sample_rect.size).GetTransformedPoints(matrix);

    for (size_t j = 0; j < 6; j++) {
      VS::PerVertexData data;
      data.position = transformed_points[indices[j]];
      data.color = colors[i].Premultiply();
      vertex_builder.AppendVertex(data);
    }
  }

  if (!vertex_builder.HasVertices()) {
    return true;
  }

  Command cmd;
  DEBUG_COMMAND_INFO(cmd, "AtlasColors");

  auto& host_buffer = pass.GetTransientsBuffer();

  VS::FrameInfo frame_info;
  frame_info.mvp = Matrix::MakeOrthographic(pass.GetRenderTargetSize()) *
                   entity.GetTransformation();

  FS::FragInfo frag_info;
  frag_info.alpha = alpha_;

  auto opts = OptionsFromPassAndEntity(pass, entity);
  opts.blend_mode = BlendMode::kSourceOver;
  cmd.pipeline = renderer.GetGeometryColorPipeline(opts);
  cmd.stencil_reference = entity.GetStencilDepth();
  cmd.BindVertices(vertex_builder.CreateVertexBuffer(host_buffer));
  VS::BindFrameInfo(cmd, host_buffer.EmplaceUniform(frame_info));
  FS::BindFragInfo(cmd, host_buffer.EmplaceUniform(frag_info));
  return pass.AddCommand(std::move(cmd));
}

}  // namespace impeller
