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

#include <cmath>
#include <memory>
#include <optional>
#include <vector>

#include "flutter/fml/logging.h"
#include "impeller/core/allocator.h"
#include "impeller/scene/importer/conversions.h"

namespace impeller {
namespace scene {

std::unique_ptr<Skin> Skin::MakeFromFlatbuffer(
    const fb::Skin& skin,
    const std::vector<std::shared_ptr<Node>>& scene_nodes) {
  if (!skin.joints() || !skin.inverse_bind_matrices() ||
      skin.joints()->size() != skin.inverse_bind_matrices()->size()) {
    VALIDATION_LOG << "Skin data is missing joints or bind matrices.";
    return nullptr;
  }

  Skin result;

  result.joints_.reserve(skin.joints()->size());
  for (auto joint : *skin.joints()) {
    if (joint < 0 || static_cast<size_t>(joint) > scene_nodes.size()) {
      VALIDATION_LOG << "Skin joint index out of range.";
      result.joints_.push_back(nullptr);
      continue;
    }
    if (scene_nodes[joint]) {
      scene_nodes[joint]->SetIsJoint(true);
    }
    result.joints_.push_back(scene_nodes[joint]);
  }

  result.inverse_bind_matrices_.reserve(skin.inverse_bind_matrices()->size());
  for (size_t matrix_i = 0; matrix_i < skin.inverse_bind_matrices()->size();
       matrix_i++) {
    const auto* ip_matrix = skin.inverse_bind_matrices()->Get(matrix_i);
    Matrix matrix = ip_matrix ? importer::ToMatrix(*ip_matrix) : Matrix();

    result.inverse_bind_matrices_.push_back(matrix);
    // Overwrite the joint transforms with the inverse bind pose.
    result.joints_[matrix_i]->SetGlobalTransform(matrix.Invert());
  }

  return std::make_unique<Skin>(std::move(result));
}

Skin::Skin() = default;

Skin::~Skin() = default;

Skin::Skin(Skin&&) = default;

Skin& Skin::operator=(Skin&&) = default;

std::shared_ptr<Texture> Skin::GetJointsTexture(Allocator& allocator) {
  // Each joint has a matrix. 1 matrix = 16 floats. 1 pixel = 4 floats.
  // Therefore, each joint needs 4 pixels.
  auto required_pixels = joints_.size() * 4;
  auto dimension_size = std::max(
      2u,
      Allocation::NextPowerOfTwoSize(std::ceil(std::sqrt(required_pixels))));

  impeller::TextureDescriptor texture_descriptor;
  texture_descriptor.storage_mode = impeller::StorageMode::kHostVisible;
  texture_descriptor.format = PixelFormat::kR32G32B32A32Float;
  texture_descriptor.size = {dimension_size, dimension_size};
  texture_descriptor.mip_count = 1u;

  auto result = allocator.CreateTexture(texture_descriptor);
  result->SetLabel("Joints Texture");
  if (!result) {
    FML_LOG(ERROR) << "Could not create joint texture.";
    return nullptr;
  }

  std::vector<Matrix> joints;
  joints.resize(result->GetSize().Area() / 4, Matrix());
  FML_DCHECK(joints.size() >= joints_.size());
  for (size_t joint_i = 0; joint_i < joints_.size(); joint_i++) {
    const Node* joint = joints_[joint_i].get();
    if (!joint) {
      // When a joint is missing, just let it remain as an identity matrix.
      continue;
    }

    // Compute a model space matrix for the joint by walking up the bones to the
    // skeleton root.
    while (joint && joint->IsJoint()) {
      joints[joint_i] = joint->GetLocalTransform() * joints[joint_i];
      joint = joint->GetParent();
    }

    // Get the joint transform relative to the default pose of the bone by
    // incorporating the joint's inverse bind matrix. The inverse bind matrix
    // transforms from model space to the default pose space of the joint. The
    // result is a model space matrix that only captures the difference between
    // the joint's default pose and the joint's current pose in the scene. This
    // is necessary because the skinned model's vertex positions (which _define_
    // the default pose) are all in model space.
    joints[joint_i] = joints[joint_i] * inverse_bind_matrices_[joint_i];
  }

  if (!result->SetContents(reinterpret_cast<uint8_t*>(joints.data()),
                           joints.size() * sizeof(Matrix))) {
    FML_LOG(ERROR) << "Could not set contents of joint texture.";
    return nullptr;
  }

  return result;
}

}  // namespace scene
}  // namespace impeller
