//----------------------------------------------------------------------------//
//                                                                            //
// 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 <algorithm>
#include <atomic>
#include <cstdlib>
#include <future>

#include "framework/application.h"
#include "framework/imgui.h"
#include "framework/renderer.h"
#include "framework/utils.h"
#include "ozz/animation/runtime/animation.h"
#include "ozz/animation/runtime/local_to_model_job.h"
#include "ozz/animation/runtime/sampling_job.h"
#include "ozz/animation/runtime/skeleton.h"
#include "ozz/base/containers/vector.h"
#include "ozz/base/log.h"
#include "ozz/base/maths/box.h"
#include "ozz/base/maths/math_ex.h"
#include "ozz/base/maths/simd_math.h"
#include "ozz/base/maths/soa_transform.h"
#include "ozz/base/maths/vec_float.h"
#include "ozz/options/options.h"

#if EMSCRIPTEN
#include <emscripten.h>
#include <emscripten/threading.h>
#endif  // EMSCRIPTEN

// Skeleton archive can be specified as an option.
OZZ_OPTIONS_DECLARE_STRING(skeleton,
                           "Path to the skeleton (ozz archive format).",
                           "media/skeleton.ozz", false)

// Animation archive can be specified as an option.
OZZ_OPTIONS_DECLARE_STRING(animation,
                           "Path to the first animation (ozz archive format).",
                           "media/animation.ozz", false)

// Interval between each character.
const float kInterval = 2.f;

// Width and depth of characters repartition.
const int kWidth = 16;
const int kDepth = 16;

// The maximum number of characters.
const int kMaxCharacters = 4096;

// The minimum number of characters per task.
const int kMinGrainSize = 32;

// Checks if platform has threading support.
bool HasThreadingSupport() {
#ifdef EMSCRIPTEN
  return emscripten_has_threading_support();
#else   // EMSCRIPTEN
  return true;
#endif  // EMSCRIPTEN
}

class MultithreadSampleApplication : public ozz::sample::Application {
 public:
  MultithreadSampleApplication()
      : characters_(kMaxCharacters),
        num_characters_(kMaxCharacters / 4),
        has_threading_support_(HasThreadingSupport()),
        enable_theading_(has_threading_support_),
        grain_size_(128) {
    if (has_threading_support_) {
      ozz::log::Out() << "Platform has threading support." << std::endl;
    } else {
      ozz::log::Out() << "Platform doesn't have threading support, "
                      << "multithreading is disabled." << std::endl;
    }
  }

 private:
  // Nested Character struct forward declaration.
  struct Character;

  static bool UpdateCharacter(const ozz::animation::Animation& _animation,
                              const ozz::animation::Skeleton& _skeleton,
                              float _dt, Character* _character) {
    // Samples animation.
    _character->controller.Update(_animation, _dt);

    // Setup sampling job.
    ozz::animation::SamplingJob sampling_job;
    sampling_job.animation = &_animation;
    sampling_job.context = &_character->context;
    sampling_job.ratio = _character->controller.time_ratio();
    sampling_job.output = make_span(_character->locals);

    // Samples animation.
    if (!sampling_job.Run()) {
      return false;
    }

    // Converts from local space to model space matrices.
    ozz::animation::LocalToModelJob ltm_job;
    ltm_job.skeleton = &_skeleton;
    ltm_job.input = make_span(_character->locals);
    ltm_job.output = make_span(_character->models);
    if (!ltm_job.Run()) {
      return false;
    }
    return true;
  }

  // Data structure used to pass const arguments to parallel tasks.
  struct ParallelArgs {
    const ozz::animation::Animation* animation;
    const ozz::animation::Skeleton* skeleton;
    float dt;
    int grain_size;  // Maximum number of characters that can be processed by a
                     // task.
  };

  // Data used to monitor and analyze threading.
  // Every task will push its thread id to an array. We can then process it to
  // find how many threads were used.
  class ParallelMonitor {
   public:
    ParallelMonitor() {
      // Finds the maximum possible number of tasks considering kMinGrain grain
      // size for kMaxCharacters characters.
      int max_tasks = 1;
      for (int processed = kMinGrainSize; processed < kMaxCharacters;
           processed *= 2, max_tasks *= 2) {
      }
      thread_ids_.resize(max_tasks);
      num_async_tasks_.store(0);
    }

    void Reset() { num_async_tasks_.store(0); }

    // Stores this thread identifier to a new task slot.
    void PushTask() {
      thread_ids_[num_async_tasks_++] = hasher_(std::this_thread::get_id());
    }

    // Finds number of threads, which is the number of unique thread ids
    // found.
    int ThreadCount() {
      std::sort(thread_ids_.begin(), thread_ids_.begin() + num_async_tasks_);
      auto end = std::unique(thread_ids_.begin(),
                             thread_ids_.begin() + num_async_tasks_);

      return static_cast<int>(end - thread_ids_.begin());
    }

    int TaskCount() const { return num_async_tasks_; }

   private:
    std::hash<std::thread::id> hasher_;
    ozz::vector<size_t> thread_ids_;
    std::atomic_uint num_async_tasks_;
  };

  static bool ParallelUpdate(const ParallelArgs& _args, Character* _characters,
                             int _num, ParallelMonitor* _monitor) {
    bool success = true;
    if (_num <= _args.grain_size) {
      _monitor->PushTask();

      for (int i = 0; i < _num; ++i) {
        success &= UpdateCharacter(*_args.animation, *_args.skeleton, _args.dt,
                                   &_characters[i]);
      }
    } else {
      // Run half the job on an async task, possibly another thread.
      const int half = _num / 2;
      auto handle = std::async(std::launch::async, ParallelUpdate, _args,
                               _characters + half, _num - half, _monitor);

      // The other half is processed by this thread.
      success &= ParallelUpdate(_args, _characters, half, _monitor);

      // Get result of the async task or process it if it's not started yet.
      success &= handle.get();
    }
    return success;
  }

  // Updates current animation time.
  virtual bool OnUpdate(float _dt, float) {
    bool success = true;
    if (enable_theading_) {
      // Initialize task counter. It's only used to monitor threading behavior.
      monitor_.Reset();

      const ParallelArgs args = {&animation_, &skeleton_, _dt, grain_size_};
      success = ParallelUpdate(args, array_begin(characters_), num_characters_,
                               &monitor_);
    } else {
      for (int i = 0; i < num_characters_; ++i) {
        success &= UpdateCharacter(animation_, skeleton_, _dt,
                                   array_begin(characters_) + i);
      }
    }
    return success;
  }

  // Renders all skeletons.
  virtual bool OnDisplay(ozz::sample::Renderer* _renderer) {
    bool success = true;
    for (int c = 0; success && c < num_characters_; ++c) {
      const ozz::math::Float4 position(
          ((c % kWidth) - kWidth / 2) * kInterval,
          ((c / kWidth) / kDepth) * kInterval,
          (((c / kWidth) % kDepth) - kDepth / 2) * kInterval, 1.f);
      const ozz::math::Float4x4 transform = ozz::math::Float4x4::Translation(
          ozz::math::simd_float4::LoadPtrU(&position.x));
      success &= _renderer->DrawPosture(
          skeleton_, make_span(characters_[c].models), transform, false);
    }

    return true;
  }

  virtual bool OnInitialize() {
    // Reading skeleton.
    if (!ozz::sample::LoadSkeleton(OPTIONS_skeleton, &skeleton_)) {
      return false;
    }

    // Reading animations.
    if (!ozz::sample::LoadAnimation(OPTIONS_animation, &animation_)) {
      return false;
    }

    // Allocate a default number of characters.
    AllocateCharaters();

    return true;
  }

  bool AllocateCharaters() {
    // Reallocate all characters.
    for (size_t c = 0; c < kMaxCharacters; ++c) {
      Character& character = characters_[c];

      // Initializes each controller start time to a different value.
      character.controller.set_time_ratio(animation_.duration() * kWidth * c /
                                          kMaxCharacters);

      character.locals.resize(skeleton_.num_soa_joints());
      character.models.resize(skeleton_.num_joints());
      character.context.Resize(animation_.num_tracks());
    }

    return true;
  }

  virtual void OnDestroy() {}

  virtual bool OnGui(ozz::sample::ImGui* _im_gui) {
    // Exposes number of characters.
    {
      static bool oc_open = true;
      ozz::sample::ImGui::OpenClose oc(_im_gui, "Sample control", &oc_open);
      if (oc_open) {
        char label[64];
        std::snprintf(label, sizeof(label), "Number of entities: %d", num_characters_);
        _im_gui->DoSlider(label, 1, kMaxCharacters, &num_characters_, .7f);
        const int num_joints = num_characters_ * skeleton_.num_joints();
        std::snprintf(label, sizeof(label), "Number of joints: %d", num_joints);
        _im_gui->DoLabel(label);
      }
    }
    // Exposes multi-threading parameters.
    {
      static bool oc_open = true;
      ozz::sample::ImGui::OpenClose oc(_im_gui, "Threading control", &oc_open);
      if (oc_open) {
        _im_gui->DoCheckBox("Enables threading", &enable_theading_,
                            has_threading_support_);
        if (enable_theading_) {
          char label[64];
          std::snprintf(label, sizeof(label), "Grain size: %d", grain_size_);
          _im_gui->DoSlider(label, kMinGrainSize, kMaxCharacters, &grain_size_,
                            .2f);
          const int num_threads = monitor_.ThreadCount();
          std::snprintf(label, sizeof(label), "Thread/task count: %d/%d", num_threads,
                       monitor_.TaskCount());
          _im_gui->DoLabel(label);
        }
      }
    }
    return true;
  }

  virtual void GetSceneBounds(ozz::math::Box* _bound) const {
    _bound->min.x = -(kWidth / 2) * kInterval;
    _bound->max.x =
        _bound->min.x + ozz::math::Min(num_characters_, kWidth) * kInterval;
    _bound->min.y = 0.f;
    _bound->max.y = ((num_characters_ / kWidth / kDepth) + 1) * kInterval;
    _bound->min.z = -(kDepth / 2) * kInterval;
    _bound->max.z =
        _bound->min.z +
        ozz::math::Min(num_characters_ / kWidth, kDepth) * kInterval;
  }

  // Runtime skeleton.
  ozz::animation::Skeleton skeleton_;

  // Runtime animation.
  ozz::animation::Animation animation_;

  // Character structure contains all the data required to sample and blend a
  // character.
  struct Character {
    // Playback animation controller. This is a utility class that helps with
    // controlling animation playback time.
    ozz::sample::PlaybackController controller;

    // Sampling context.
    ozz::animation::SamplingJob::Context context;

    // Buffer of local transforms which stores the blending result.
    ozz::vector<ozz::math::SoaTransform> locals;

    // Buffer of model space matrices. These are computed by the local-to-model
    // job after the blending stage.
    ozz::vector<ozz::math::Float4x4> models;
  };

  // Array of characters of the sample.
  ozz::vector<Character> characters_;

  // Number of used characters.
  int num_characters_;

  // Does the current plateform actually has threading support.
  bool has_threading_support_;

  // Enable or disable threading.
  bool enable_theading_;

  // Define the number of characters that a task can handle.
  int grain_size_;

  // Data used to monitor and analyze threading.
  ParallelMonitor monitor_;
};

int main(int _argc, const char** _argv) {
  const char* title = "Ozz-animation sample: Multi-threading";
  return MultithreadSampleApplication().Run(_argc, _argv, "1.0", title);
}
