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

#include <array>
#include <cstring>
#include <functional>
#include <iostream>
#include <iterator>
#include <memory>
#include <vector>

#include "flutter/fml/mapping.h"
#include "impeller/geometry/matrix.h"
#include "impeller/scene/importer/conversions.h"
#include "impeller/scene/importer/scene_flatbuffers.h"
#include "impeller/scene/importer/vertices_builder.h"
#include "third_party/tinygltf/tiny_gltf.h"

namespace impeller {
namespace scene {
namespace importer {

static const std::map<std::string, VerticesBuilder::AttributeType> kAttributes =
    {{"POSITION", VerticesBuilder::AttributeType::kPosition},
     {"NORMAL", VerticesBuilder::AttributeType::kNormal},
     {"TANGENT", VerticesBuilder::AttributeType::kTangent},
     {"TEXCOORD_0", VerticesBuilder::AttributeType::kTextureCoords},
     {"COLOR_0", VerticesBuilder::AttributeType::kColor},
     {"JOINTS_0", VerticesBuilder::AttributeType::kJoints},
     {"WEIGHTS_0", VerticesBuilder::AttributeType::kWeights}};

static bool WithinRange(int index, size_t size) {
  return index >= 0 && static_cast<size_t>(index) < size;
}

static bool MeshPrimitiveIsSkinned(const tinygltf::Primitive& primitive) {
  return primitive.attributes.find("JOINTS_0") != primitive.attributes.end() &&
         primitive.attributes.find("WEIGHTS_0") != primitive.attributes.end();
}

static void ProcessMaterial(const tinygltf::Model& gltf,
                            const tinygltf::Material& in_material,
                            fb::MaterialT& out_material) {
  out_material.type = fb::MaterialType::kUnlit;
  out_material.base_color_factor =
      ToFBColor(in_material.pbrMetallicRoughness.baseColorFactor);
  bool base_color_texture_valid =
      in_material.pbrMetallicRoughness.baseColorTexture.texCoord == 0 &&
      in_material.pbrMetallicRoughness.baseColorTexture.index >= 0 &&
      in_material.pbrMetallicRoughness.baseColorTexture.index <
          static_cast<int32_t>(gltf.textures.size());
  out_material.base_color_texture =
      base_color_texture_valid
          // This is safe because every GLTF input texture is mapped to a
          // `Scene->texture`.
          ? in_material.pbrMetallicRoughness.baseColorTexture.index
          : -1;
}

static bool ProcessMeshPrimitive(const tinygltf::Model& gltf,
                                 const tinygltf::Primitive& primitive,
                                 fb::MeshPrimitiveT& mesh_primitive) {
  //---------------------------------------------------------------------------
  /// Vertices.
  ///

  {
    bool is_skinned = MeshPrimitiveIsSkinned(primitive);
    std::unique_ptr<VerticesBuilder> builder =
        is_skinned ? VerticesBuilder::MakeSkinned()
                   : VerticesBuilder::MakeUnskinned();

    for (const auto& attribute : primitive.attributes) {
      auto attribute_type = kAttributes.find(attribute.first);
      if (attribute_type == kAttributes.end()) {
        std::cerr << "Vertex attribute \"" << attribute.first
                  << "\" not supported." << std::endl;
        continue;
      }
      if (!is_skinned &&
          (attribute_type->second == VerticesBuilder::AttributeType::kJoints ||
           attribute_type->second ==
               VerticesBuilder::AttributeType::kWeights)) {
        // If the primitive doesn't have enough information to be skinned, skip
        // skinning-related attributes.
        continue;
      }

      const auto accessor = gltf.accessors[attribute.second];
      const auto view = gltf.bufferViews[accessor.bufferView];

      const auto buffer = gltf.buffers[view.buffer];
      const unsigned char* source_start = &buffer.data[view.byteOffset];

      VerticesBuilder::ComponentType type;
      switch (accessor.componentType) {
        case TINYGLTF_COMPONENT_TYPE_BYTE:
          type = VerticesBuilder::ComponentType::kSignedByte;
          break;
        case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE:
          type = VerticesBuilder::ComponentType::kUnsignedByte;
          break;
        case TINYGLTF_COMPONENT_TYPE_SHORT:
          type = VerticesBuilder::ComponentType::kSignedShort;
          break;
        case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT:
          type = VerticesBuilder::ComponentType::kUnsignedShort;
          break;
        case TINYGLTF_COMPONENT_TYPE_INT:
          type = VerticesBuilder::ComponentType::kSignedInt;
          break;
        case TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT:
          type = VerticesBuilder::ComponentType::kUnsignedInt;
          break;
        case TINYGLTF_COMPONENT_TYPE_FLOAT:
          type = VerticesBuilder::ComponentType::kFloat;
          break;
        default:
          std::cerr << "Skipping attribute \"" << attribute.first
                    << "\" due to invalid component type." << std::endl;
          continue;
      }

      builder->SetAttributeFromBuffer(
          attribute_type->second,     // attribute
          type,                       // component_type
          source_start,               // buffer_start
          accessor.ByteStride(view),  // stride_bytes
          accessor.count);            // count
    }

    builder->WriteFBVertices(mesh_primitive);
  }

  //---------------------------------------------------------------------------
  /// Indices.
  ///

  {
    if (!WithinRange(primitive.indices, gltf.accessors.size())) {
      std::cerr << "Mesh primitive has no index buffer. Skipping." << std::endl;
      return false;
    }

    auto index_accessor = gltf.accessors[primitive.indices];
    auto index_view = gltf.bufferViews[index_accessor.bufferView];

    auto indices = std::make_unique<fb::IndicesT>();

    switch (index_accessor.componentType) {
      case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT:
        indices->type = fb::IndexType::k16Bit;
        break;
      case TINYGLTF_COMPONENT_TYPE_UNSIGNED_INT:
        indices->type = fb::IndexType::k32Bit;
        break;
      default:
        std::cerr << "Mesh primitive has unsupported index type "
                  << index_accessor.componentType << ". Skipping.";
        return false;
    }
    indices->count = index_accessor.count;
    indices->data.resize(index_view.byteLength);
    const auto* index_buffer =
        &gltf.buffers[index_view.buffer].data[index_view.byteOffset];
    std::memcpy(indices->data.data(), index_buffer, indices->data.size());

    mesh_primitive.indices = std::move(indices);
  }

  //---------------------------------------------------------------------------
  /// Material.
  ///

  {
    auto material = std::make_unique<fb::MaterialT>();
    if (primitive.material >= 0 &&
        primitive.material < static_cast<int>(gltf.materials.size())) {
      ProcessMaterial(gltf, gltf.materials[primitive.material], *material);
    } else {
      material->type = fb::MaterialType::kUnlit;
    }
    mesh_primitive.material = std::move(material);
  }

  return true;
}

static void ProcessNode(const tinygltf::Model& gltf,
                        const tinygltf::Node& in_node,
                        fb::NodeT& out_node) {
  out_node.name = in_node.name;
  out_node.children = in_node.children;

  //---------------------------------------------------------------------------
  /// Transform.
  ///

  Matrix transform;
  if (in_node.scale.size() == 3) {
    transform =
        transform * Matrix::MakeScale({static_cast<Scalar>(in_node.scale[0]),
                                       static_cast<Scalar>(in_node.scale[1]),
                                       static_cast<Scalar>(in_node.scale[2])});
  }
  if (in_node.rotation.size() == 4) {
    transform = transform * Matrix::MakeRotation(Quaternion(
                                in_node.rotation[0], in_node.rotation[1],
                                in_node.rotation[2], in_node.rotation[3]));
  }
  if (in_node.translation.size() == 3) {
    transform = transform * Matrix::MakeTranslation(
                                {static_cast<Scalar>(in_node.translation[0]),
                                 static_cast<Scalar>(in_node.translation[1]),
                                 static_cast<Scalar>(in_node.translation[2])});
  }
  if (in_node.matrix.size() == 16) {
    if (!transform.IsIdentity()) {
      std::cerr << "The `matrix` attribute of node (name: " << in_node.name
                << ") is set in addition to one or more of the "
                   "`translation/rotation/scale` attributes. Using only the "
                   "`matrix` "
                   "attribute.";
    }
    transform = ToMatrix(in_node.matrix);
  }
  out_node.transform = ToFBMatrixUniquePtr(transform);

  //---------------------------------------------------------------------------
  /// Static meshes.
  ///

  if (WithinRange(in_node.mesh, gltf.meshes.size())) {
    auto& mesh = gltf.meshes[in_node.mesh];
    for (const auto& primitive : mesh.primitives) {
      auto mesh_primitive = std::make_unique<fb::MeshPrimitiveT>();
      if (!ProcessMeshPrimitive(gltf, primitive, *mesh_primitive)) {
        continue;
      }
      out_node.mesh_primitives.push_back(std::move(mesh_primitive));
    }
  }

  //---------------------------------------------------------------------------
  /// Skin.
  ///

  if (WithinRange(in_node.skin, gltf.skins.size())) {
    auto& skin = gltf.skins[in_node.skin];

    auto ipskin = std::make_unique<fb::SkinT>();
    ipskin->joints = skin.joints;
    {
      std::vector<fb::Matrix> matrices;
      auto& matrix_accessor = gltf.accessors[skin.inverseBindMatrices];
      auto& matrix_view = gltf.bufferViews[matrix_accessor.bufferView];
      auto& matrix_buffer = gltf.buffers[matrix_view.buffer];
      for (size_t matrix_i = 0; matrix_i < matrix_accessor.count; matrix_i++) {
        auto* s = reinterpret_cast<const float*>(
            matrix_buffer.data.data() + matrix_view.byteOffset +
            matrix_accessor.ByteStride(matrix_view) * matrix_i);
        Matrix m(s[0], s[1], s[2], s[3],    //
                 s[4], s[5], s[6], s[7],    //
                 s[8], s[9], s[10], s[11],  //
                 s[12], s[13], s[14], s[15]);
        matrices.push_back(ToFBMatrix(m));
      }
      ipskin->inverse_bind_matrices = std::move(matrices);
    }
    ipskin->skeleton = skin.skeleton;
    out_node.skin = std::move(ipskin);
  }
}

static void ProcessTexture(const tinygltf::Model& gltf,
                           const tinygltf::Texture& in_texture,
                           fb::TextureT& out_texture) {
  if (!WithinRange(in_texture.source, gltf.images.size())) {
    return;
  }
  auto& image = gltf.images[in_texture.source];

  auto embedded = std::make_unique<fb::EmbeddedImageT>();
  embedded->bytes = image.image;
  size_t bytes_per_component = 0;
  switch (image.pixel_type) {
    case TINYGLTF_COMPONENT_TYPE_UNSIGNED_BYTE:
      embedded->component_type = fb::ComponentType::k8Bit;
      bytes_per_component = 1;
      break;
    case TINYGLTF_COMPONENT_TYPE_UNSIGNED_SHORT:
      embedded->component_type = fb::ComponentType::k16Bit;
      bytes_per_component = 2;
      break;
    default:
      std::cerr << "Texture component type " << image.pixel_type
                << " not supported." << std::endl;
      return;
  }
  if (image.image.size() !=
      bytes_per_component * image.component * image.width * image.height) {
    std::cerr << "Decompressed texture had unexpected buffer size. Skipping."
              << std::endl;
    return;
  }
  embedded->component_count = image.component;
  embedded->width = image.width;
  embedded->height = image.height;
  out_texture.embedded_image = std::move(embedded);
  out_texture.uri = image.uri;
}

static void ProcessAnimation(const tinygltf::Model& gltf,
                             const tinygltf::Animation& in_animation,
                             fb::AnimationT& out_animation) {
  out_animation.name = in_animation.name;

  // std::vector<impeller::fb::ChannelT> channels;
  std::vector<impeller::fb::ChannelT> translation_channels;
  std::vector<impeller::fb::ChannelT> rotation_channels;
  std::vector<impeller::fb::ChannelT> scale_channels;
  for (auto& in_channel : in_animation.channels) {
    auto out_channel = fb::ChannelT();

    out_channel.node = in_channel.target_node;
    auto& sampler = in_animation.samplers[in_channel.sampler];

    /// Keyframe times.
    auto& times_accessor = gltf.accessors[sampler.input];
    if (times_accessor.count <= 0) {
      continue;  // Nothing to record.
    }
    {
      auto& times_bufferview = gltf.bufferViews[times_accessor.bufferView];
      auto& times_buffer = gltf.buffers[times_bufferview.buffer];
      if (times_accessor.componentType != TINYGLTF_COMPONENT_TYPE_FLOAT) {
        std::cerr << "Unexpected component type \""
                  << times_accessor.componentType
                  << "\" for animation channel times accessor. Skipping."
                  << std::endl;
        continue;
      }
      if (times_accessor.type != TINYGLTF_TYPE_SCALAR) {
        std::cerr << "Unexpected type \"" << times_accessor.type
                  << "\" for animation channel times accessor. Skipping."
                  << std::endl;
        continue;
      }
      for (size_t time_i = 0; time_i < times_accessor.count; time_i++) {
        const float* time_p = reinterpret_cast<const float*>(
            times_buffer.data.data() + times_bufferview.byteOffset +
            times_accessor.ByteStride(times_bufferview) * time_i);
        out_channel.timeline.push_back(*time_p);
      }
    }

    /// Keyframe values.
    auto& values_accessor = gltf.accessors[sampler.output];
    if (values_accessor.count != times_accessor.count) {
      std::cerr << "Mismatch between time and value accessors for animation "
                   "channel. Skipping."
                << std::endl;
      continue;
    }
    {
      auto& values_bufferview = gltf.bufferViews[values_accessor.bufferView];
      auto& values_buffer = gltf.buffers[values_bufferview.buffer];
      if (values_accessor.componentType != TINYGLTF_COMPONENT_TYPE_FLOAT) {
        std::cerr << "Unexpected component type \""
                  << values_accessor.componentType
                  << "\" for animation channel values accessor. Skipping."
                  << std::endl;
        continue;
      }
      if (in_channel.target_path == "translation") {
        if (values_accessor.type != TINYGLTF_TYPE_VEC3) {
          std::cerr << "Unexpected type \"" << values_accessor.type
                    << "\" for animation channel \"translation\" accessor. "
                       "Skipping."
                    << std::endl;
          continue;
        }
        fb::TranslationKeyframesT keyframes;
        for (size_t value_i = 0; value_i < values_accessor.count; value_i++) {
          const float* value_p = reinterpret_cast<const float*>(
              values_buffer.data.data() + values_bufferview.byteOffset +
              values_accessor.ByteStride(values_bufferview) * value_i);
          keyframes.values.push_back(
              fb::Vec3(value_p[0], value_p[1], value_p[2]));
        }
        out_channel.keyframes.Set(std::move(keyframes));
        translation_channels.push_back(std::move(out_channel));
      } else if (in_channel.target_path == "rotation") {
        if (values_accessor.type != TINYGLTF_TYPE_VEC4) {
          std::cerr << "Unexpected type \"" << values_accessor.type
                    << "\" for animation channel \"rotation\" accessor. "
                       "Skipping."
                    << std::endl;
          continue;
        }
        fb::RotationKeyframesT keyframes;
        for (size_t value_i = 0; value_i < values_accessor.count; value_i++) {
          const float* value_p = reinterpret_cast<const float*>(
              values_buffer.data.data() + values_bufferview.byteOffset +
              values_accessor.ByteStride(values_bufferview) * value_i);
          keyframes.values.push_back(
              fb::Vec4(value_p[0], value_p[1], value_p[2], value_p[3]));
        }
        out_channel.keyframes.Set(std::move(keyframes));
        rotation_channels.push_back(std::move(out_channel));
      } else if (in_channel.target_path == "scale") {
        if (values_accessor.type != TINYGLTF_TYPE_VEC3) {
          std::cerr << "Unexpected type \"" << values_accessor.type
                    << "\" for animation channel \"scale\" accessor. "
                       "Skipping."
                    << std::endl;
          continue;
        }
        fb::ScaleKeyframesT keyframes;
        for (size_t value_i = 0; value_i < values_accessor.count; value_i++) {
          const float* value_p = reinterpret_cast<const float*>(
              values_buffer.data.data() + values_bufferview.byteOffset +
              values_accessor.ByteStride(values_bufferview) * value_i);
          keyframes.values.push_back(
              fb::Vec3(value_p[0], value_p[1], value_p[2]));
        }
        out_channel.keyframes.Set(std::move(keyframes));
        scale_channels.push_back(std::move(out_channel));
      } else {
        std::cerr << "Unsupported animation channel target path \""
                  << in_channel.target_path << "\". Skipping." << std::endl;
        continue;
      }
    }
  }

  std::vector<std::unique_ptr<impeller::fb::ChannelT>> channels;
  for (const auto& channel_list :
       {translation_channels, rotation_channels, scale_channels}) {
    for (const auto& channel : channel_list) {
      channels.push_back(std::make_unique<fb::ChannelT>(channel));
    }
  }
  out_animation.channels = std::move(channels);
}

bool ParseGLTF(const fml::Mapping& source_mapping, fb::SceneT& out_scene) {
  tinygltf::Model gltf;

  {
    tinygltf::TinyGLTF loader;
    std::string error;
    std::string warning;
    bool success = loader.LoadBinaryFromMemory(&gltf, &error, &warning,
                                               source_mapping.GetMapping(),
                                               source_mapping.GetSize());
    if (!warning.empty()) {
      std::cerr << "Warning while loading GLTF: " << warning << std::endl;
    }
    if (!error.empty()) {
      std::cerr << "Error while loading GLTF: " << error << std::endl;
    }
    if (!success) {
      return false;
    }
  }

  const tinygltf::Scene& scene = gltf.scenes[gltf.defaultScene];
  out_scene.children = scene.nodes;

  out_scene.transform =
      ToFBMatrixUniquePtr(Matrix::MakeScale(Vector3(1, 1, -1)));

  for (size_t texture_i = 0; texture_i < gltf.textures.size(); texture_i++) {
    auto texture = std::make_unique<fb::TextureT>();
    ProcessTexture(gltf, gltf.textures[texture_i], *texture);
    out_scene.textures.push_back(std::move(texture));
  }

  for (size_t node_i = 0; node_i < gltf.nodes.size(); node_i++) {
    auto node = std::make_unique<fb::NodeT>();
    ProcessNode(gltf, gltf.nodes[node_i], *node);
    out_scene.nodes.push_back(std::move(node));
  }

  for (size_t animation_i = 0; animation_i < gltf.animations.size();
       animation_i++) {
    auto animation = std::make_unique<fb::AnimationT>();
    ProcessAnimation(gltf, gltf.animations[animation_i], *animation);
    out_scene.animations.push_back(std::move(animation));
  }

  return true;
}

}  // namespace importer
}  // namespace scene
}  // namespace impeller
