/**
Copyright (C) 2024  Matthew Kosarek

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
**/

#define MIR_LOG_COMPONENT "animator"
#define GLM_ENABLE_EXPERIMENTAL

#include "animator.h"
#include <algorithm>
#include <chrono>
#include <glm/gtx/transform.hpp>
#include <mir/log.h>
#include <mir/server_action_queue.h>
#include <utility>
#include <vector>

using namespace miracle;
using namespace std::chrono_literals;
namespace geom = mir::geometry;

namespace
{
glm::vec2 to_glm_vec2(mir::geometry::Point const& p)
{
    return { p.x.as_int(), p.y.as_int() };
}

float get_percent_complete(float const target, float const real)
{
    if (target == 0)
        return 1.f;

    float const percent = real / target;
    if (std::isinf(percent) != 0 || percent > 1.f)
        return 1.f;
    else
        return percent;
}

float ease_out_bounce(AnimationDefinition const& defintion, float x)
{
    if (x < 1 / defintion.d1)
    {
        return defintion.n1 * x * x;
    }
    else if (x < 2 / defintion.d1)
    {
        return defintion.n1 * (x -= 1.5f / defintion.d1) * x + 0.75f;
    }
    else if (x < 2.5 / defintion.d1)
    {
        return defintion.n1 * (x -= 2.25f / defintion.d1) * x + 0.9375f;
    }
    else
    {
        return defintion.n1 * (x -= 2.625f / defintion.d1) * x + 0.984375f;
    }
}

float ease(AnimationDefinition const& defintion, float t)
{
    // https://easings.net/
    switch (defintion.function)
    {
    case EaseFunction::linear:
        return t;
    case EaseFunction::ease_in_sine:
        return 1 - cosf((t * static_cast<float>(M_PI)) / 2.f);
    case EaseFunction::ease_in_out_sine:
        return -(cosf(static_cast<float>(M_PI) * t) - 1) / 2;
    case EaseFunction::ease_out_sine:
        return sinf((t * static_cast<float>(M_PI)) / 2.f);
    case EaseFunction::ease_in_quad:
        return t * t;
    case EaseFunction::ease_out_quad:
        return 1 - (1 - t) * (1 - t);
    case EaseFunction::ease_in_out_quad:
        return t < 0.5 ? 2 * t * t : 1 - powf(-2 * t + 2, 2) / 2;
    case EaseFunction::ease_in_cubic:
        return t * t * t;
    case EaseFunction::ease_out_cubic:
        return 1 - powf(1 - t, 3);
    case EaseFunction::ease_in_out_cubic:
        return t < 0.5 ? 4 * t * t * t : 1 - powf(-2 * t + 2, 3) / 2;
    case EaseFunction::ease_in_quart:
        return t * t * t * t;
    case EaseFunction::ease_out_quart:
        return 1 - powf(1 - t, 4);
    case EaseFunction::ease_in_out_quart:
        return t < 0.5 ? 8 * t * t * t * t : 1 - powf(-2 * t + 2, 4) / 2;
    case EaseFunction::ease_in_quint:
        return t * t * t * t * t;
    case EaseFunction::ease_out_quint:
        return 1 - powf(1 - t, 5);
    case EaseFunction::ease_in_out_quint:
        return t < 0.5 ? 16 * t * t * t * t * t : 1 - powf(-2 * t + 2, 5) / 2;
    case EaseFunction::ease_in_expo:
        return t == 0 ? 0 : powf(2, 10 * t - 10);
    case EaseFunction::ease_out_expo:
        return t == 1 ? 1 : 1 - powf(2, -10 * t);
    case EaseFunction::ease_in_out_expo:
        return t == 0
            ? 0
            : t == 1
            ? 1
            : t < 0.5 ? powf(2, 20 * t - 10) / 2
                      : (2 - powf(2, -20 * t + 10)) / 2;
    case EaseFunction::ease_in_circ:
        return 1 - sqrtf(1 - powf(t, 2));
    case EaseFunction::ease_out_circ:
        return sqrtf(1 - powf(t - 1, 2));
    case EaseFunction::ease_in_out_circ:
        return t < 0.5f
            ? (1 - sqrtf(1 - powf(2 * t, 2))) / 2
            : (sqrtf(1 - powf(-2 * t + 2, 2)) + 1) / 2;
    case EaseFunction::ease_in_back:
        return defintion.c3 * t * t * t - defintion.c1 * t * t;
    case EaseFunction::ease_out_back:
    {
        return 1 + defintion.c3 * powf(t - 1, 3) + defintion.c1 * powf(t - 1, 2);
    }
    case EaseFunction::ease_in_out_back:
        return t < 0.5
            ? (powf(2 * t, 2) * ((defintion.c2 + 1) * 2 * t - defintion.c2)) / 2
            : (powf(2 * t - 2, 2) * ((defintion.c2 + 1) * (t * 2 - 2) + defintion.c2) + 2) / 2;
    case EaseFunction::ease_in_elastic:
        return t == 0
            ? 0
            : t == 1
            ? 1
            : -powf(2, 10 * t - 10) * sinf((t * 10 - 10.75f) * defintion.c4);
    case EaseFunction::ease_out_elastic:
        return t == 0
            ? 0
            : t == 1
            ? 1
            : powf(2, -10 * t) * sinf((t * 10 - 0.75f) * defintion.c4) + 1;
    case EaseFunction::ease_in_out_elastic:
        return t == 0
            ? 0
            : t == 1
            ? 1
            : t < 0.5
            ? -(powf(2, 20 * t - 10) * sinf((20 * t - 11.125f) * defintion.c5)) / 2
            : (powf(2, -20 * t + 10) * sinf((20 * t - 11.125f) * defintion.c5)) / 2 + 1;
    case EaseFunction::ease_in_bounce:
        return 1 - ease_out_bounce(defintion, 1 - t);
    case EaseFunction::ease_out_bounce:
        return ease_out_bounce(defintion, t);
    case EaseFunction::ease_in_out_bounce:
        return t < 0.5
            ? (1 - ease_out_bounce(defintion, 1 - 2 * t)) / 2
            : (1 + ease_out_bounce(defintion, 2 * t - 1)) / 2;
    default:
        return 1.f;
    }
}

float interpolate_scale(float const p, float const start, float const end)
{
    float const diff = end - start;
    if (diff == 0)
        return 1.f;

    // We want to find the percentage that we should scale relative
    // to the [start] value. For example, if we are growing from 200
    // to 250, and p=0.5, then we should be at width 225, which would
    // be a scale up of 225 / 220;
    float const current = start + diff * p;
    return current / end;
}

float interpolate_scale2(float const p, float const start, float const end, float const real)
{
    float const diff = end - start;
    if (diff == 0)
        return 1.f;

    float const current = start + diff * p;
    return current / real;
}

struct SlideResult
{
    /// The current position that the surface should be in.
    /// This should also be used as the clip area position.
    glm::vec2 position;

    /// The current size of the clip area. The surface should NOT
    /// be set to this size, as it has already been set on init().
    /// This size is strictly meant for the clip area.
    glm::vec2 clip_area_size;

    /// The transformation ao apply to the surface.
    glm::mat4 transform;
};

inline SlideResult slide(float p, geom::Rectangle const& from, geom::Rectangle const& to, geom::Size const& committed_size)
{
    auto const distance = to.top_left - from.top_left;
    float const dx = static_cast<float>(distance.dx.as_int()) * p;
    float const dy = static_cast<float>(distance.dy.as_int()) * p;

    float const clip_scale_x = interpolate_scale(p, static_cast<float>(from.size.width.as_value()), static_cast<float>(to.size.width.as_value()));
    float const clip_scale_y = interpolate_scale(p, static_cast<float>(from.size.height.as_value()), static_cast<float>(to.size.height.as_value()));

    // This bit will only make sense by example.
    //
    // Let's say we're growing the width from 50px to 100px. When we first start animating,
    // the client will not have yet confirmed the size, so it will most be 50px.
    // In this case, [real_scale_x] will still be 200%, since it will want to scale from 50px
    // to 100px (assuming p=0).
    //
    // However, after a frame or two, the actual size of the window will be 100px. In this
    // case, we will want to scale down by ~50% (assuming p~=0) from 100px to ~50px.
    float const real_scale_x = interpolate_scale2(
        p,
        static_cast<float>(from.size.width.as_value()),
        static_cast<float>(to.size.width.as_value()),
        static_cast<float>(committed_size.width.as_value()));
    float const real_scale_y = interpolate_scale2(
        p,
        static_cast<float>(from.size.height.as_value()),
        static_cast<float>(to.size.height.as_value()),
        static_cast<float>(committed_size.height.as_value()));

    return {
        .position = glm::vec2(static_cast<float>(from.top_left.x.as_int()) + dx, static_cast<float>(from.top_left.y.as_int()) + dy),
        .clip_area_size = glm::vec2(static_cast<float>(to.size.width.as_int()) * clip_scale_x, static_cast<float>(to.size.height.as_int()) * clip_scale_y),
        .transform = glm::scale(glm::mat4(1.0), glm::vec3(real_scale_x, real_scale_y, 0.f))
    };
}

glm::vec2 to_vec2_point(geom::Rectangle const& r)
{
    return { r.top_left.x.as_int(), r.top_left.y.as_int() };
}

glm::vec2 to_vec2_size(geom::Rectangle const& r)
{
    return { r.size.width.as_int(), r.size.height.as_int() };
}
}

AnimationHandle const miracle::none_animation_handle = 0;

Animation::Animation(
    AnimationHandle handle,
    AnimationDefinition definition,
    mir::geometry::Rectangle const& from,
    mir::geometry::Rectangle const& to,
    mir::geometry::Rectangle const& current) :
    handle { handle },
    definition { std::move(definition) },
    to { to },
    from { current },
    clip_area { current },
    runtime_seconds { 0.f },
    real_size { current.size }
{
    switch (definition.type)
    {
    case AnimationType::slide:
    {
        // Find out the percentage that we're already through the move. This could be negative, by design.
        glm::vec2 end = to_glm_vec2(to.top_left);
        glm::vec2 start = to_glm_vec2(from.top_left);
        glm::vec2 real_start = to_glm_vec2(current.top_left);
        auto percent_x = get_percent_complete(end.x - start.x, real_start.x - start.x);
        auto percent_y = get_percent_complete(end.y - start.y, real_start.y - start.y);

        // Find out the percentage that we're already through the resize. This could be negative, by design.
        float width_change = to.size.width.as_int() - from.size.width.as_int();
        float height_change = to.size.height.as_int() - from.size.height.as_int();
        float real_width_change = current.size.width.as_int() - from.size.width.as_int();
        float real_height_change = current.size.height.as_int() - from.size.height.as_int();

        float percent_w = get_percent_complete(width_change, real_width_change);
        float percent_h = get_percent_complete(height_change, real_height_change);

        float percentage = std::min(percent_x, std::min(percent_y, std::min(percent_w, percent_h)));
        percentage = std::clamp(percentage, 0.f, 1.f);
        runtime_seconds = percentage * definition.duration_seconds;
        break;
    }
    default:
        break;
    }
}

AnimationStepResult Animation::init()
{
    switch (definition.type)
    {
    case AnimationType::grow:
        return { handle, false, clip_area, std::nullopt, std::nullopt, glm::mat4(0.f) };
    case AnimationType::shrink:
        return { handle, false, clip_area, std::nullopt, std::nullopt, glm::mat4(1.f) };
    case AnimationType::slide:
    {
        // Sliding is funky. We resize immediately but remain in the same position. The transformation
        // and position are interpolated over time to give the illusion of moving and growing.
        auto result = slide(0, from, to, real_size);
        return { handle, false, clip_area, result.position, to_vec2_size(to), result.transform };
    }
    case AnimationType::fade_in:
        return { .handle = handle, .is_complete = false, .clip_area = clip_area, .opacity = 0 };
    case AnimationType::fade_out:
        return { .handle = handle, .is_complete = false, .clip_area = clip_area, .opacity = 1 };
    case AnimationType::disabled:
        return { handle, true, clip_area, to_vec2_point(to), to_vec2_size(to), glm::mat4(1.f) };
    default:
        return { handle, false, clip_area, std::nullopt, std::nullopt, std::nullopt };
    }
}

AnimationStepResult Animation::step(float dt)
{
    runtime_seconds += dt;
    float const t = (runtime_seconds / definition.duration_seconds);

    if (runtime_seconds >= definition.duration_seconds)
    {
        return { handle, true, to, to_vec2_point(to), to_vec2_size(to), glm::mat4(1.f) };
    }

    switch (definition.type)
    {
    case AnimationType::slide:
    {
        auto const p = ease(definition, t);
        const auto [position, clip_area_size, transform] = slide(p, from, to, real_size);
        clip_area.top_left.x = geom::X { position.x };
        clip_area.top_left.y = geom::Y { position.y };
        clip_area.size.width = geom::Width { clip_area_size.x };
        clip_area.size.height = geom::Height { clip_area_size.y };
        return {
            handle,
            false,
            clip_area,
            position,
            std::nullopt,
            transform,
            1.f
        };
    }
    case AnimationType::grow:
    {
        auto const p = ease(definition, t);
        glm::vec3 const translate(
            static_cast<float>(to.size.width.as_value()) / 2.f,
            static_cast<float>(to.size.height.as_value()) / 2.f,
            0);
        auto const inverse_translate = -translate;
        glm::mat4 const transform = glm::translate(
            glm::scale(
                glm::translate(translate),
                glm::vec3(p, p, 1.f)),
            inverse_translate);
        return { handle, false, to, std::nullopt, std::nullopt, transform };
    }
    case AnimationType::shrink:
    {
        auto const p = 1.f - ease(definition, t);
        glm::vec3 const translate(
            static_cast<float>(from.size.width.as_value()) / 2.f,
            static_cast<float>(from.size.height.as_value()) / 2.f,
            0);
        auto const inverse_translate = -translate;
        glm::mat4 const transform = glm::translate(
            glm::scale(
                glm::translate(translate),
                glm::vec3(p, p, 1.f)),
            inverse_translate);
        return { handle, false, to, std::nullopt, std::nullopt, transform };
    }
    case AnimationType::fade_in:
    {
        auto const p = ease(definition, t);
        return { .handle = handle, .is_complete = false, .opacity = p };
    }
    case AnimationType::fade_out:
    {
        auto const p = 1.f - ease(definition, t);
        return { .handle = handle, .is_complete = false, .opacity = p };
    }
    case AnimationType::disabled:
    default:
        return { handle, true, to, std::nullopt, std::nullopt, std::nullopt };
    }
}

void Animation::set_current_size(mir::geometry::Size const& size)
{
    real_size = size;
}

void Animation::mark_for_great_animator_in_the_sky()
{
    should_leave_this_animator_for_the_great_animator_in_the_sky = true;
}

bool Animation::is_going_to_great_animator_in_the_sky() const
{
    return should_leave_this_animator_for_the_great_animator_in_the_sky;
}

AnimationHandle Animator::register_animateable()
{
    std::lock_guard<std::mutex> lock(processing_lock);
    return next_handle++;
}

void Animator::append(std::shared_ptr<Animation> const& animation)
{
    std::lock_guard<std::mutex> lock(processing_lock);
    for (auto const& other : active)
    {
        if (other->get_handle() == animation->get_handle())
            other->mark_for_great_animator_in_the_sky();
    }
    active.push_back(animation);
    animation->on_tick(animation->init());
    cv.notify_one();
}

void Animator::tick(float dt)
{
    std::lock_guard<std::mutex> lock(processing_lock);

    for (auto const& item : active)
    {
        if (item->is_going_to_great_animator_in_the_sky())
            continue;

        auto result = item->step(dt);

        item->on_tick(result);

        if (result.is_complete)
            item->mark_for_great_animator_in_the_sky();
    }

    std::erase_if(active, [](std::shared_ptr<Animation> const& animation)
    {
        return animation->is_going_to_great_animator_in_the_sky();
    });
}

void Animator::set_size_hack(AnimationHandle handle, mir::geometry::Size const& size)
{
    std::lock_guard<std::mutex> lock(processing_lock);

    for (auto const& animation : active)
    {
        if (animation->get_handle() == handle)
            animation->set_current_size(size);
    }
}

void Animator::remove_by_animation_handle(AnimationHandle handle)
{
    std::lock_guard<std::mutex> lock(processing_lock);
    for (auto const& animation : active)
    {
        if (animation->get_handle() == handle)
            animation->mark_for_great_animator_in_the_sky();
    }
}

bool Animator::is_animating(AnimationHandle handle)
{
    std::lock_guard<std::mutex> lock(processing_lock);
    return std::any_of(active.begin(), active.end(), [handle](std::shared_ptr<Animation> const& animation)
    {
        return animation->get_handle() == handle;
    });
}
