// 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/scene/geometry.h"

#include <iostream>
#include <memory>
#include <ostream>

#include "impeller/core/device_buffer_descriptor.h"
#include "impeller/core/formats.h"
#include "impeller/core/sampler_descriptor.h"
#include "impeller/core/vertex_buffer.h"
#include "impeller/geometry/point.h"
#include "impeller/geometry/vector.h"
#include "impeller/renderer/sampler_library.h"
#include "impeller/renderer/vertex_buffer_builder.h"
#include "impeller/scene/importer/scene_flatbuffers.h"
#include "impeller/scene/shaders/skinned.vert.h"
#include "impeller/scene/shaders/unskinned.vert.h"

namespace impeller {
namespace scene {

//------------------------------------------------------------------------------
/// Geometry
///

Geometry::~Geometry() = default;

std::shared_ptr<CuboidGeometry> Geometry::MakeCuboid(Vector3 size) {
  auto result = std::make_shared<CuboidGeometry>();
  result->SetSize(size);
  return result;
}

std::shared_ptr<Geometry> Geometry::MakeVertexBuffer(VertexBuffer vertex_buffer,
                                                     bool is_skinned) {
  if (is_skinned) {
    auto result = std::make_shared<SkinnedVertexBufferGeometry>();
    result->SetVertexBuffer(std::move(vertex_buffer));
    return result;
  } else {
    auto result = std::make_shared<UnskinnedVertexBufferGeometry>();
    result->SetVertexBuffer(std::move(vertex_buffer));
    return result;
  }
}

std::shared_ptr<Geometry> Geometry::MakeFromFlatbuffer(
    const fb::MeshPrimitive& mesh,
    Allocator& allocator) {
  IndexType index_type;
  switch (mesh.indices()->type()) {
    case fb::IndexType::k16Bit:
      index_type = IndexType::k16bit;
      break;
    case fb::IndexType::k32Bit:
      index_type = IndexType::k32bit;
      break;
  }

  const uint8_t* vertices_start;
  size_t vertices_bytes;
  bool is_skinned;

  switch (mesh.vertices_type()) {
    case fb::VertexBuffer::UnskinnedVertexBuffer: {
      const auto* vertices =
          mesh.vertices_as_UnskinnedVertexBuffer()->vertices();
      vertices_start = reinterpret_cast<const uint8_t*>(vertices->Get(0));
      vertices_bytes = vertices->size() * sizeof(fb::Vertex);
      is_skinned = false;
      break;
    }
    case fb::VertexBuffer::SkinnedVertexBuffer: {
      const auto* vertices = mesh.vertices_as_SkinnedVertexBuffer()->vertices();
      vertices_start = reinterpret_cast<const uint8_t*>(vertices->Get(0));
      vertices_bytes = vertices->size() * sizeof(fb::SkinnedVertex);
      is_skinned = true;
      break;
    }
    case fb::VertexBuffer::NONE:
      VALIDATION_LOG << "Invalid vertex buffer type.";
      return nullptr;
  }

  const uint8_t* indices_start =
      reinterpret_cast<const uint8_t*>(mesh.indices()->data()->Data());

  const size_t indices_bytes = mesh.indices()->data()->size();
  if (vertices_bytes == 0 || indices_bytes == 0) {
    return nullptr;
  }

  DeviceBufferDescriptor buffer_desc;
  buffer_desc.size = vertices_bytes + indices_bytes;
  buffer_desc.storage_mode = StorageMode::kHostVisible;

  auto buffer = allocator.CreateBuffer(buffer_desc);
  buffer->SetLabel("Mesh vertices+indices");

  if (!buffer->CopyHostBuffer(vertices_start, Range(0, vertices_bytes))) {
    return nullptr;
  }
  if (!buffer->CopyHostBuffer(indices_start, Range(0, indices_bytes),
                              vertices_bytes)) {
    return nullptr;
  }

  VertexBuffer vertex_buffer = {
      .vertex_buffer = {.buffer = buffer, .range = Range(0, vertices_bytes)},
      .index_buffer = {.buffer = buffer,
                       .range = Range(vertices_bytes, indices_bytes)},
      .vertex_count = mesh.indices()->count(),
      .index_type = index_type,
  };
  return MakeVertexBuffer(std::move(vertex_buffer), is_skinned);
}

void Geometry::SetJointsTexture(const std::shared_ptr<Texture>& texture) {}

//------------------------------------------------------------------------------
/// CuboidGeometry
///

CuboidGeometry::CuboidGeometry() = default;

CuboidGeometry::~CuboidGeometry() = default;

void CuboidGeometry::SetSize(Vector3 size) {
  size_ = size;
}

// |Geometry|
GeometryType CuboidGeometry::GetGeometryType() const {
  return GeometryType::kUnskinned;
}

// |Geometry|
VertexBuffer CuboidGeometry::GetVertexBuffer(Allocator& allocator) const {
  VertexBufferBuilder<UnskinnedVertexShader::PerVertexData, uint16_t> builder;
  // Layout: position, normal, tangent, uv
  builder.AddVertices({
      // Front.
      {Vector3(0, 0, 0), Vector3(0, 0, -1), Vector3(1, 0, 0), Point(0, 0),
       Color::White()},
      {Vector3(1, 0, 0), Vector3(0, 0, -1), Vector3(1, 0, 0), Point(1, 0),
       Color::White()},
      {Vector3(1, 1, 0), Vector3(0, 0, -1), Vector3(1, 0, 0), Point(1, 1),
       Color::White()},
      {Vector3(1, 1, 0), Vector3(0, 0, -1), Vector3(1, 0, 0), Point(1, 1),
       Color::White()},
      {Vector3(0, 1, 0), Vector3(0, 0, -1), Vector3(1, 0, 0), Point(0, 1),
       Color::White()},
      {Vector3(0, 0, 0), Vector3(0, 0, -1), Vector3(1, 0, 0), Point(0, 0),
       Color::White()},
  });
  return builder.CreateVertexBuffer(allocator);
}

// |Geometry|
void CuboidGeometry::BindToCommand(const SceneContext& scene_context,
                                   HostBuffer& buffer,
                                   const Matrix& transform,
                                   Command& command) const {
  command.BindVertices(
      GetVertexBuffer(*scene_context.GetContext()->GetResourceAllocator()));

  UnskinnedVertexShader::FrameInfo info;
  info.mvp = transform;
  UnskinnedVertexShader::BindFrameInfo(command, buffer.EmplaceUniform(info));
}

//------------------------------------------------------------------------------
/// UnskinnedVertexBufferGeometry
///

UnskinnedVertexBufferGeometry::UnskinnedVertexBufferGeometry() = default;

UnskinnedVertexBufferGeometry::~UnskinnedVertexBufferGeometry() = default;

void UnskinnedVertexBufferGeometry::SetVertexBuffer(
    VertexBuffer vertex_buffer) {
  vertex_buffer_ = std::move(vertex_buffer);
}

// |Geometry|
GeometryType UnskinnedVertexBufferGeometry::GetGeometryType() const {
  return GeometryType::kUnskinned;
}

// |Geometry|
VertexBuffer UnskinnedVertexBufferGeometry::GetVertexBuffer(
    Allocator& allocator) const {
  return vertex_buffer_;
}

// |Geometry|
void UnskinnedVertexBufferGeometry::BindToCommand(
    const SceneContext& scene_context,
    HostBuffer& buffer,
    const Matrix& transform,
    Command& command) const {
  command.BindVertices(
      GetVertexBuffer(*scene_context.GetContext()->GetResourceAllocator()));

  UnskinnedVertexShader::FrameInfo info;
  info.mvp = transform;
  UnskinnedVertexShader::BindFrameInfo(command, buffer.EmplaceUniform(info));
}

//------------------------------------------------------------------------------
/// SkinnedVertexBufferGeometry
///

SkinnedVertexBufferGeometry::SkinnedVertexBufferGeometry() = default;

SkinnedVertexBufferGeometry::~SkinnedVertexBufferGeometry() = default;

void SkinnedVertexBufferGeometry::SetVertexBuffer(VertexBuffer vertex_buffer) {
  vertex_buffer_ = std::move(vertex_buffer);
}

// |Geometry|
GeometryType SkinnedVertexBufferGeometry::GetGeometryType() const {
  return GeometryType::kSkinned;
}

// |Geometry|
VertexBuffer SkinnedVertexBufferGeometry::GetVertexBuffer(
    Allocator& allocator) const {
  return vertex_buffer_;
}

// |Geometry|
void SkinnedVertexBufferGeometry::BindToCommand(
    const SceneContext& scene_context,
    HostBuffer& buffer,
    const Matrix& transform,
    Command& command) const {
  command.BindVertices(
      GetVertexBuffer(*scene_context.GetContext()->GetResourceAllocator()));

  SamplerDescriptor sampler_desc;
  sampler_desc.min_filter = MinMagFilter::kNearest;
  sampler_desc.mag_filter = MinMagFilter::kNearest;
  sampler_desc.mip_filter = MipFilter::kNearest;
  sampler_desc.width_address_mode = SamplerAddressMode::kRepeat;
  sampler_desc.label = "NN Repeat";

  SkinnedVertexShader::BindJointsTexture(
      command,
      joints_texture_ ? joints_texture_ : scene_context.GetPlaceholderTexture(),
      scene_context.GetContext()->GetSamplerLibrary()->GetSampler(
          sampler_desc));

  SkinnedVertexShader::FrameInfo info;
  info.mvp = transform;
  info.enable_skinning = joints_texture_ ? 1 : 0;
  info.joint_texture_size =
      joints_texture_ ? joints_texture_->GetSize().width : 1;
  SkinnedVertexShader::BindFrameInfo(command, buffer.EmplaceUniform(info));
}

// |Geometry|
void SkinnedVertexBufferGeometry::SetJointsTexture(
    const std::shared_ptr<Texture>& texture) {
  joints_texture_ = texture;
}
}  // namespace scene
}  // namespace impeller
