/*Copyright (C) 2010-2023 Duncan Deveaux

This file is part of Hikou no mizu.

Hikou no mizu 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.

Hikou no mizu 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 Hikou no mizu.  If not, see <http://www.gnu.org/licenses/>.
*/

#include "FightCameraHandler.hpp"
#include "Player/Player.hpp"

#include "Graphics/Camera.hpp"
#include <algorithm>
#include <cstddef>
#include <cmath>

namespace
{
    /// Padding between the camera box and the players contained in it
    constexpr float PADDING = 175.f;

    constexpr float PI = 3.14159265f;
    constexpr float SHAKE_LENGTH = 250.f; ///< Length of the shake in ms
    constexpr float SHAKE_AMPLITUDE = .01f;

    /// Zoom a box to a sub-scale value
    Box zoomBox(const Box &box, float scale)
    {
        return (scale >= 1.f) ? box :
                                Box(box.left + box.width * (1.f - scale) / 2.f,
                                    box.top + box.height * (1.f - scale) / 2.f,
                                    box.width * scale, box.height * scale);
    }

    /// Compute the amount of zoom / shaking that will be applied
    /// based on elapsed time since shake start
    float computeShakeAmount(float shakeElapsedTime)
    {
        const float x_shake = shakeElapsedTime * (PI / SHAKE_LENGTH);
        const float zoomLevel = SHAKE_AMPLITUDE *
            static_cast<float>(sin(x_shake) + .7f * sin(5.f * x_shake)) /
            (2 * x_shake + 1.f);

        return (zoomLevel >= 0.f) ? zoomLevel : 0.f;
    }

    /// Apply shaking to a box based on the known shake starting times
    /// and the max intensity inferred from the number of players
    Box shakeBox(const Box &box,
                 std::list<Timer::TimePoint> &shakeTimes,
                 std::size_t playersCount)
    {
        float shakeAmount = 0.f;

        //Cumulate the impact of known shakes
        std::list<Timer::TimePoint>::const_iterator it = shakeTimes.begin();
        while (it != shakeTimes.end())
        {
            const float shakeElapsedTime = Timer::duration_float_ms(
                Timer::getTime() - shakeTimes.front()).count();

            if (shakeElapsedTime >= SHAKE_LENGTH || shakeElapsedTime < 0.f)
                break;

            shakeAmount += computeShakeAmount(shakeElapsedTime);
            ++it;
        }

        //Erase completed shakes
        if (it != shakeTimes.end())
            shakeTimes.erase(it, shakeTimes.end());

        //Weigh the shaking strength by the amount of players in the game
        if (playersCount > 2)
            shakeAmount /= static_cast<float>(playersCount) / 2.f;

        return zoomBox(box, 1.f - shakeAmount);
    }
}

FightCameraHandler::FightCameraHandler() :
m_currentBox(Box(0.f, 0.f, -1.f, -1.f)),
m_mode(FightCameraHandler::CameraMode::Static)
{
    setArena(Box(0.f, 0.f, 1920.f, 1080.f), 1000.f);
}

void FightCameraHandler::setArena(const Box &arenaBox, float groundLevel)
{
    m_groundLevel = groundLevel;
    m_arenaBox = arenaBox;
    m_arenaRatio = (arenaBox.height > 0.f) ? arenaBox.width / arenaBox.height
                                           : 1.f;
    m_currentBox = Box(0.f, 0.f, -1.f, -1.f);
}

void FightCameraHandler::setPlayers(const std::vector<const Player*> &players)
{
    m_players = players;
}

void FightCameraHandler::setMode(CameraMode mode)
{
    m_mode = mode;
}

void FightCameraHandler::shake()
{
    //Add a new camera shake beginning now
    m_shakeTimes.push_front(Timer::getTime());
}

void FightCameraHandler::look(float viewWidth, float viewHeight,
                              float frameTime, float arenaMinCameraWidth)
{
    //Skip the computation if the view is too large for the arena
    if (m_mode == FightCameraHandler::CameraMode::Dynamic &&
        m_arenaBox.width > arenaMinCameraWidth &&
        m_arenaBox.width > viewWidth && m_arenaBox.height > viewHeight)
    {
        Box view(0.f, 0.f, viewWidth, viewHeight);
        computeView(view, (viewWidth > arenaMinCameraWidth) ? viewWidth
                                                  : arenaMinCameraWidth);

        //Update current and target boxes
        m_targetBox = view;
        if (m_currentBox.width < 0.f)
            m_currentBox = m_targetBox; //Initialization of m_currentBox
        else
        {
            //Fade m_currentBox into m_targetBox
            const float fadeRatio = (frameTime / 16.67f) * .1f;
            m_currentBox.fadeInto(m_targetBox, (fadeRatio < 1.f) ? fadeRatio
                                                                 : 1.f);
        }
    }
    else
    {
        m_targetBox = m_arenaBox;
        m_currentBox = m_arenaBox;
    }

    Camera::look(shakeBox(m_currentBox, m_shakeTimes, m_players.size()),
                 viewWidth, viewHeight);
}

const Box &FightCameraHandler::getCurrentView() const
{
    return m_currentBox;
}

void FightCameraHandler::computeView(Box &view, float minWidth) const
{
    emcompass(view);
    extend(view, minWidth);
    adjust(view);
    center(view);
}

void FightCameraHandler::emcompass(Box &box) const
{
    if (!m_players.empty())
    {
        const Box &box0 = m_players[0]->getBox();

        float left = box0.left, top = box0.top,
            right = box0.left + box0.width, bottom = box0.top + box0.height;

        for (std::size_t i = 1; i < m_players.size(); i++)
        {
            const Box &p_box = m_players[i]->getBox();

            if (p_box.left < left) left = p_box.left;
            if (p_box.top < top) top = p_box.top;
            if (p_box.left + p_box.width > right) right = p_box.left + p_box.width;
            if (p_box.top + p_box.height > bottom) bottom = p_box.top + p_box.height;
        }

        box = Box(left, top, right - left, bottom - top);
    }
}

void FightCameraHandler::extend(Box &box, float minWidth) const
{
    //Always show the ground
    box.height += (m_arenaBox.top + m_arenaBox.height) - (box.top + box.height);

    //Add some padding
    float width_left = m_arenaBox.width - box.width,
        height_left = m_arenaBox.height - box.height;

    float side_padding = std::min(width_left / 2 - 1, PADDING);
    float top_padding = std::min(height_left, PADDING);

    box.left -= side_padding;
    box.width += side_padding * 2;
    box.top -= top_padding;
    box.height += top_padding;

    //Respect the minimum width
    if (box.width < minWidth)
    {
        box.left -= (minWidth - box.width) / 2;
        box.width = minWidth;
    }
}

void FightCameraHandler::adjust(Box &box) const
{
    const float ratio = box.width / box.height;

    if (ratio > m_arenaRatio) //Height should be adjusted
        box.height = box.width / m_arenaRatio;
    else if (ratio < m_arenaRatio) //Width should be adjusted
        box.width = m_arenaRatio * box.height;
}

void FightCameraHandler::center(Box &box) const
{
    //Keeps the ground of the arena at the same place relative to the area
    if (m_arenaBox.top + m_arenaBox.height > 0)
    {
        const float ratioGround = (m_arenaBox.top + m_groundLevel) /
                                  (m_arenaBox.top + m_arenaBox.height);
        box.top = (m_arenaBox.top + m_groundLevel) - ratioGround * box.height;
    }

    //Fixes box out of the arena box
    if (box.left < m_arenaBox.left)
        box.left = m_arenaBox.left;

    if (box.top < m_arenaBox.top)
        box.top = m_arenaBox.top;

    if (box.left + box.width > m_arenaBox.left + m_arenaBox.width)
        box.left -= (box.left + box.width) - (m_arenaBox.left + m_arenaBox.width);

    if (box.top + box.height > m_arenaBox.top + m_arenaBox.height)
        box.top -= (box.top + box.height) - (m_arenaBox.top + m_arenaBox.height);
}
