// Copyright 2017 Citra Emulator Project
// Licensed under GPLv2 or any later version
// Refer to the license.txt file included.

#include <chrono>
#include <mutex>
#include <thread>
#include <tuple>
#include "common/math_util.h"
#include "common/quaternion.h"
#include "common/thread.h"
#include "common/vector_math.h"
#include "input_common/motion_emu.h"

namespace InputCommon {

// Implementation class of the motion emulation device
class MotionEmuDevice {
public:
    MotionEmuDevice(int update_millisecond, float sensitivity)
        : update_millisecond(update_millisecond),
          update_duration(std::chrono::duration_cast<std::chrono::steady_clock::duration>(
              std::chrono::milliseconds(update_millisecond))),
          sensitivity(sensitivity), motion_emu_thread(&MotionEmuDevice::MotionEmuThread, this) {}

    ~MotionEmuDevice() {
        if (motion_emu_thread.joinable()) {
            shutdown_event.Set();
            motion_emu_thread.join();
        }
    }

    void BeginTilt(int x, int y) {
        mouse_origin = Math::MakeVec(x, y);
        is_tilting = true;
    }

    void Tilt(int x, int y) {
        auto mouse_move = Math::MakeVec(x, y) - mouse_origin;
        if (is_tilting) {
            std::lock_guard<std::mutex> guard(tilt_mutex);
            if (mouse_move.x == 0 && mouse_move.y == 0) {
                tilt_angle = 0;
            } else {
                tilt_direction = mouse_move.Cast<float>();
                tilt_angle = MathUtil::Clamp(tilt_direction.Normalize() * sensitivity, 0.0f,
                                             MathUtil::PI * 0.5f);
            }
        }
    }

    void EndTilt() {
        std::lock_guard<std::mutex> guard(tilt_mutex);
        tilt_angle = 0;
        is_tilting = false;
    }

    std::tuple<Math::Vec3<float>, Math::Vec3<float>> GetStatus() {
        std::lock_guard<std::mutex> guard(status_mutex);
        return status;
    }

private:
    const int update_millisecond;
    const std::chrono::steady_clock::duration update_duration;
    const float sensitivity;

    Math::Vec2<int> mouse_origin;

    std::mutex tilt_mutex;
    Math::Vec2<float> tilt_direction;
    float tilt_angle = 0;

    bool is_tilting = false;

    Common::Event shutdown_event;
    std::thread motion_emu_thread;

    std::tuple<Math::Vec3<float>, Math::Vec3<float>> status;
    std::mutex status_mutex;

    void MotionEmuThread() {
        auto update_time = std::chrono::steady_clock::now();
        Math::Quaternion<float> q = MakeQuaternion(Math::Vec3<float>(), 0);
        Math::Quaternion<float> old_q;

        while (!shutdown_event.WaitUntil(update_time)) {
            update_time += update_duration;
            old_q = q;

            {
                std::lock_guard<std::mutex> guard(tilt_mutex);

                // Find the quaternion describing current 3DS tilting
                q = MakeQuaternion(Math::MakeVec(-tilt_direction.y, 0.0f, tilt_direction.x),
                                   tilt_angle);
            }

            auto inv_q = q.Inverse();

            // Set the gravity vector in world space
            auto gravity = Math::MakeVec(0.0f, -1.0f, 0.0f);

            // Find the angular rate vector in world space
            auto angular_rate = ((q - old_q) * inv_q).xyz * 2;
            angular_rate *= 1000 / update_millisecond / MathUtil::PI * 180;

            // Transform the two vectors from world space to 3DS space
            gravity = QuaternionRotate(inv_q, gravity);
            angular_rate = QuaternionRotate(inv_q, angular_rate);

            // Update the sensor state
            {
                std::lock_guard<std::mutex> guard(status_mutex);
                status = std::make_tuple(gravity, angular_rate);
            }
        }
    }
};

// Interface wrapper held by input receiver as a unique_ptr. It holds the implementation class as
// a shared_ptr, which is also observed by the factory class as a weak_ptr. In this way the factory
// can forward all the inputs to the implementation only when it is valid.
class MotionEmuDeviceWrapper : public Input::MotionDevice {
public:
    MotionEmuDeviceWrapper(int update_millisecond, float sensitivity) {
        device = std::make_shared<MotionEmuDevice>(update_millisecond, sensitivity);
    }

    std::tuple<Math::Vec3<float>, Math::Vec3<float>> GetStatus() const {
        return device->GetStatus();
    }

    std::shared_ptr<MotionEmuDevice> device;
};

std::unique_ptr<Input::MotionDevice> MotionEmu::Create(const Common::ParamPackage& params) {
    int update_period = params.Get("update_period", 100);
    float sensitivity = params.Get("sensitivity", 0.01f);
    auto device_wrapper = std::make_unique<MotionEmuDeviceWrapper>(update_period, sensitivity);
    // Previously created device is disconnected here. Having two motion devices for 3DS is not
    // expected.
    current_device = device_wrapper->device;
    return std::move(device_wrapper);
}

void MotionEmu::BeginTilt(int x, int y) {
    if (auto ptr = current_device.lock()) {
        ptr->BeginTilt(x, y);
    }
}

void MotionEmu::Tilt(int x, int y) {
    if (auto ptr = current_device.lock()) {
        ptr->Tilt(x, y);
    }
}

void MotionEmu::EndTilt() {
    if (auto ptr = current_device.lock()) {
        ptr->EndTilt();
    }
}

} // namespace InputCommon
