//----------------------------------------------------------------------------//
//                                                                            //
// ozz-animation is hosted at http://github.com/guillaumeblanc/ozz-animation  //
// and distributed under the MIT License (MIT).                               //
//                                                                            //
// Copyright (c) Guillaume Blanc                                              //
//                                                                            //
// Permission is hereby granted, free of charge, to any person obtaining a    //
// copy of this software and associated documentation files (the "Software"), //
// to deal in the Software without restriction, including without limitation  //
// the rights to use, copy, modify, merge, publish, distribute, sublicense,   //
// and/or sell copies of the Software, and to permit persons to whom the      //
// Software is furnished to do so, subject to the following conditions:       //
//                                                                            //
// The above copyright notice and this permission notice shall be included in //
// all copies or substantial portions of the Software.                        //
//                                                                            //
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR //
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,   //
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL    //
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER //
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING    //
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER        //
// DEALINGS IN THE SOFTWARE.                                                  //
//                                                                            //
//----------------------------------------------------------------------------//

#include "ozz/animation/offline/fbx/fbx.h"

#include "ozz/base/log.h"
#include "ozz/base/maths/transform.h"
#include "ozz/base/memory/allocator.h"

namespace ozz {
namespace animation {
namespace offline {
namespace fbx {

FbxManagerInstance::FbxManagerInstance() : fbx_manager_(nullptr) {
  // Instantiate Fbx manager, mostly a memory manager.
  fbx_manager_ = FbxManager::Create();

  // Logs SDK version.
  ozz::log::Log() << "FBX importer version " << fbx_manager_->GetVersion()
                  << "." << std::endl;
}

FbxManagerInstance::~FbxManagerInstance() {
  // Destroy the manager and all associated objects.
  fbx_manager_->Destroy();
  fbx_manager_ = nullptr;
}

FbxDefaultIOSettings::FbxDefaultIOSettings(const FbxManagerInstance& _manager)
    : io_settings_(nullptr) {
  io_settings_ = FbxIOSettings::Create(_manager, IOSROOT);
  io_settings_->SetBoolProp(IMP_FBX_MATERIAL, false);
  io_settings_->SetBoolProp(IMP_FBX_TEXTURE, false);
  io_settings_->SetBoolProp(IMP_FBX_MODEL, false);
  io_settings_->SetBoolProp(IMP_FBX_SHAPE, false);
  io_settings_->SetBoolProp(IMP_FBX_LINK, false);
  io_settings_->SetBoolProp(IMP_FBX_GOBO, false);
}

FbxDefaultIOSettings::~FbxDefaultIOSettings() {
  io_settings_->Destroy();
  io_settings_ = nullptr;
}

FbxAnimationIOSettings::FbxAnimationIOSettings(
    const FbxManagerInstance& _manager)
    : FbxDefaultIOSettings(_manager) {}

FbxSkeletonIOSettings::FbxSkeletonIOSettings(const FbxManagerInstance& _manager)
    : FbxDefaultIOSettings(_manager) {
  settings()->SetBoolProp(IMP_FBX_ANIMATION, false);
}

FbxSceneLoader::FbxSceneLoader(const char* _filename, const char* _password,
                               const FbxManagerInstance& _manager,
                               const FbxDefaultIOSettings& _io_settings)
    : scene_(nullptr), converter_(nullptr) {
  // Create an importer.
  FbxImporter* importer = FbxImporter::Create(_manager, "ozz file importer");
  const bool initialized = importer->Initialize(_filename, -1, _io_settings);

  ImportScene(importer, initialized, _password, _manager, _io_settings);

  // Destroy the importer
  importer->Destroy();
}

FbxSceneLoader::FbxSceneLoader(FbxStream* _stream, const char* _password,
                               const FbxManagerInstance& _manager,
                               const FbxDefaultIOSettings& _io_settings)
    : scene_(nullptr), converter_(nullptr) {
  // Create an importer.
  FbxImporter* importer = FbxImporter::Create(_manager, "ozz stream importer");
  const bool initialized =
      importer->Initialize(_stream, nullptr, -1, _io_settings);

  ImportScene(importer, initialized, _password, _manager, _io_settings);

  // Destroy the importer
  importer->Destroy();
}

void FbxSceneLoader::ImportScene(FbxImporter* _importer,
                                 const bool _initialized, const char* _password,
                                 const FbxManagerInstance& _manager,
                                 const FbxDefaultIOSettings& _io_settings) {
  // Get the version of the FBX file format.
  int major, minor, revision;
  _importer->GetFileVersion(major, minor, revision);

  if (!_initialized)  // Problem with the file to be imported.
  {
    const FbxString error = _importer->GetStatus().GetErrorString();
    ozz::log::Err() << "FbxImporter initialization failed with error: "
                    << error.Buffer() << std::endl;

    if (_importer->GetStatus().GetCode() == FbxStatus::eInvalidFileVersion) {
      ozz::log::Err() << "FBX file version is " << major << "." << minor << "."
                      << revision << "." << std::endl;
    }
  } else {
    if (_importer->IsFBX()) {
      ozz::log::Log() << "FBX file version is " << major << "." << minor << "."
                      << revision << "." << std::endl;
    }

    // Load the scene.
    scene_ = FbxScene::Create(_manager, "ozz scene");
    bool imported = _importer->Import(scene_);

    if (!imported &&  // The import file may have a password
        _importer->GetStatus().GetCode() == FbxStatus::ePasswordError) {
      _io_settings.settings()->SetStringProp(IMP_FBX_PASSWORD, _password);
      _io_settings.settings()->SetBoolProp(IMP_FBX_PASSWORD_ENABLE, true);

      // Retries to import the scene.
      imported = _importer->Import(scene_);

      if (!imported &&
          _importer->GetStatus().GetCode() == FbxStatus::ePasswordError) {
        ozz::log::Err() << "Incorrect password." << std::endl;

        // scene_ will be destroyed because imported is false.
      }
    }

    // Setup axis and system converter.
    if (imported) {
      FbxGlobalSettings& settings = scene_->GetGlobalSettings();
      converter_ = New<FbxSystemConverter>(settings.GetAxisSystem(),
                                           settings.GetSystemUnit());
    }

    // Clear the scene if import failed.
    if (!imported) {
      scene_->Destroy();
      scene_ = nullptr;
    }
  }
}

FbxSceneLoader::~FbxSceneLoader() {
  if (scene_) {
    scene_->Destroy();
    scene_ = nullptr;
  }

  if (converter_) {
    ozz::Delete(converter_);
    converter_ = nullptr;
  }
}

namespace {
ozz::math::Float4x4 BuildAxisSystemMatrix(const FbxAxisSystem& _system) {
  int sign;
  ozz::math::SimdFloat4 up = ozz::math::simd_float4::y_axis();
  ozz::math::SimdFloat4 at = ozz::math::simd_float4::z_axis();

  // The EUpVector specifies which axis has the up and down direction in the
  // system (typically this is the Y or Z axis). The sign of the EUpVector is
  // applied to represent the direction (1 is up and -1 is down relative to the
  // observer).
  const FbxAxisSystem::EUpVector eup = _system.GetUpVector(sign);
  switch (eup) {
    case FbxAxisSystem::eXAxis: {
      up = math::simd_float4::Load(1.f * sign, 0.f, 0.f, 0.f);
      // If the up axis is X, the remain two axes will be Y And Z, so the
      // ParityEven is Y, and the ParityOdd is Z
      if (_system.GetFrontVector(sign) == FbxAxisSystem::eParityEven) {
        at = math::simd_float4::Load(0.f, 1.f * sign, 0.f, 0.f);
      } else {
        at = math::simd_float4::Load(0.f, 0.f, 1.f * sign, 0.f);
      }
      break;
    }
    case FbxAxisSystem::eYAxis: {
      up = math::simd_float4::Load(0.f, 1.f * sign, 0.f, 0.f);
      // If the up axis is Y, the remain two axes will X And Z, so the
      // ParityEven is X, and the ParityOdd is Z
      if (_system.GetFrontVector(sign) == FbxAxisSystem::eParityEven) {
        at = math::simd_float4::Load(1.f * sign, 0.f, 0.f, 0.f);
      } else {
        at = math::simd_float4::Load(0.f, 0.f, 1.f * sign, 0.f);
      }
      break;
    }
    case FbxAxisSystem::eZAxis: {
      up = math::simd_float4::Load(0.f, 0.f, 1.f * sign, 0.f);
      // If the up axis is Z, the remain two axes will X And Y, so the
      // ParityEven is X, and the ParityOdd is Y
      if (_system.GetFrontVector(sign) == FbxAxisSystem::eParityEven) {
        at = math::simd_float4::Load(1.f * sign, 0.f, 0.f, 0.f);
      } else {
        at = math::simd_float4::Load(0.f, 1.f * sign, 0.f, 0.f);
      }
      break;
    }
    default: {
      assert(false && "Invalid FbxAxisSystem");
      break;
    }
  }

  // If the front axis and the up axis are determined, the third axis will be
  // automatically determined as the left one. The ECoordSystem enum is a
  // parameter to determine the direction of the third axis just as the
  // EUpVector sign. It determines if the axis system is right-handed or
  // left-handed just as the enum values.
  ozz::math::SimdFloat4 right;
  if (_system.GetCoorSystem() == FbxAxisSystem::eRightHanded) {
    right = math::Cross3(up, at);
  } else {
    right = math::Cross3(at, up);
  }

  const ozz::math::Float4x4 matrix = {
      {right, up, at, ozz::math::simd_float4::w_axis()}};

  return matrix;
}
}  // namespace

FbxSystemConverter::FbxSystemConverter(const FbxAxisSystem& _from_axis,
                                       const FbxSystemUnit& _from_unit) {
  // Build axis system conversion matrix.
  const math::Float4x4 from_matrix = BuildAxisSystemMatrix(_from_axis);

  // Finds unit conversion ratio to meters, where GetScaleFactor() is in cm.
  const float to_meters =
      static_cast<float>(_from_unit.GetScaleFactor()) * .01f;

  // Builds conversion matrices.
  convert_ = Invert(from_matrix) *
             math::Float4x4::Scaling(math::simd_float4::Load1(to_meters));
  inverse_convert_ = Invert(convert_);
  inverse_transpose_convert_ = Transpose(inverse_convert_);
}

math::Float4x4 FbxSystemConverter::ConvertMatrix(const FbxAMatrix& _m) const {
  const ozz::math::Float4x4 m = {{
      ozz::math::simd_float4::Load(
          static_cast<float>(_m[0][0]), static_cast<float>(_m[0][1]),
          static_cast<float>(_m[0][2]), static_cast<float>(_m[0][3])),
      ozz::math::simd_float4::Load(
          static_cast<float>(_m[1][0]), static_cast<float>(_m[1][1]),
          static_cast<float>(_m[1][2]), static_cast<float>(_m[1][3])),
      ozz::math::simd_float4::Load(
          static_cast<float>(_m[2][0]), static_cast<float>(_m[2][1]),
          static_cast<float>(_m[2][2]), static_cast<float>(_m[2][3])),
      ozz::math::simd_float4::Load(
          static_cast<float>(_m[3][0]), static_cast<float>(_m[3][1]),
          static_cast<float>(_m[3][2]), static_cast<float>(_m[3][3])),
  }};
  return convert_ * m * inverse_convert_;
}

math::Float3 FbxSystemConverter::ConvertPoint(const FbxVector4& _p) const {
  const math::SimdFloat4 p_in = math::simd_float4::Load(
      static_cast<float>(_p[0]), static_cast<float>(_p[1]),
      static_cast<float>(_p[2]), 1.f);
  const math::SimdFloat4 p_out = convert_ * p_in;
  math::Float3 ret;
  math::Store3PtrU(p_out, &ret.x);
  return ret;
}

math::Float3 FbxSystemConverter::ConvertVector(const FbxVector4& _p) const {
  const math::SimdFloat4 p_in = math::simd_float4::Load(
      static_cast<float>(_p[0]), static_cast<float>(_p[1]),
      static_cast<float>(_p[2]), 0.f);
  const math::SimdFloat4 p_out = inverse_transpose_convert_ * p_in;
  math::Float3 ret;
  math::Store3PtrU(p_out, &ret.x);
  return ret;
}

bool FbxSystemConverter::ConvertTransform(const FbxAMatrix& _m,
                                          math::Transform* _transform) const {
  assert(_transform);

  const math::Float4x4 matrix = ConvertMatrix(_m);

  math::SimdFloat4 translation, rotation, scale;
  if (ToAffine(matrix, &translation, &rotation, &scale)) {
    ozz::math::Transform transform;
    math::Store3PtrU(translation, &_transform->translation.x);
    math::StorePtrU(math::Normalize4(rotation), &_transform->rotation.x);
    math::Store3PtrU(scale, &_transform->scale.x);
    return true;
  }

  // Failed to decompose matrix, reset transform to identity.
  *_transform = ozz::math::Transform::identity();
  return false;
}
}  // namespace fbx
}  // namespace offline
}  // namespace animation
}  // namespace ozz
