/*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 "Client.hpp"
#include "Engines/Resources/JoystickManager.hpp"
#include "Engines/Resources/TextureManager.hpp"
#include "Engines/Resources/FontManager.hpp"
#include "Engines/Sound/SoundEngine.hpp"
#include "Graphics/Viewport.hpp"

#include "Tools/Log.hpp"
#include "Tools/Format.hpp"
#include <cassert>

Client::Client(Configuration &conf, const Viewport &viewport,
               JoystickManager &joystickManager, TextureManager &textureManager,
               FontManager &fontManager, SoundEngine &soundEngine) :
m_clientLobby(conf, viewport, textureManager, fontManager, soundEngine),
m_clientGame(m_host, joystickManager, conf, viewport.getTargetSize()),
m_exited(false)
{
    m_host.setConnectionReceiver(this);
    m_clientLobby.setLobbyCallbackReceiver(this);

    switchPhase(Client::Phase::Disconnected);
}

bool Client::init()
{
    assert(m_phase == Client::Phase::Disconnected);

    return m_host.init();
}

void Client::disconnectNow()
{
    m_host.disconnectNow();
}

void Client::lobbyConnectionRequestedCallback(const Address &address)
{
    //Disconnect immediately if a new connection request has been issued
    //while disconnecting from the previous session
    if (m_phase == Client::Phase::Disconnecting)
    {
        m_host.reset();
        switchPhase(Client::Phase::Disconnected);
    }

    connect(address);
}

void Client::lobbyConnectionCancelledCallback()
{
    m_host.disconnect(KickReason::ClientDecision);
}

void Client::lobbyExitRequestedCallback()
{
    m_exited = true;
}

void Client::serverConnectedCallback()
{
    assert(m_phase == Client::Phase::Connecting);

    Log::info("Connection successful!");
    m_clientGame.initialize(m_clientLobby.getCharacter());
    switchPhase(Client::Phase::Connected);
}

void Client::serverDisconnectingCallback(KickReason reason)
{
    Log::info(Format::format("Disconnecting from server: {}",
                             KickInfo::describe(reason)));

    switchPhase(Client::Phase::Disconnecting);
    m_clientGame.reset();
    m_clientLobby.switchToDisconnected(reason);
    m_lastHostRequestTime = Timer::getTime();
}

void Client::serverDisconnectedCallback(KickReason reason)
{
    Log::info(Format::format("Disconnected from server: {}",
                             KickInfo::describe(reason)));

    //Disconnecting without prior disconnecting notice
    if (m_phase != Client::Phase::Disconnecting)
    {
        m_clientGame.reset();
        m_clientLobby.switchToDisconnected(reason);
    }

    switchPhase(Client::Phase::Disconnected);
}

void Client::mouseMove(const Vector &viewCoords)
{
    if (m_phase != Client::Phase::Connected)
        m_clientLobby.mouseMove(viewCoords);
    else
        m_clientGame.mouseMove(viewCoords);
}

void Client::mouseRelease(const Vector &viewCoords)
{
    if (m_phase != Client::Phase::Connected)
        m_clientLobby.mouseRelease(viewCoords);
    else
        m_clientGame.mouseRelease(viewCoords);
}

void Client::keyEvent(const SDL_Event &event)
{
    if (m_phase != Client::Phase::Connected)
        m_clientLobby.keyEvent(event);
    else
        m_clientGame.keyEvent(event);
}

void Client::update(float frameTime)
{
    if (m_phase != Client::Phase::Disconnected)
        m_host.listen();

    constexpr std::chrono::seconds timeout(5);
    if ((m_phase == Client::Phase::Connecting ||
         m_phase == Client::Phase::Disconnecting) &&
        (Timer::getTime() - m_lastHostRequestTime) > timeout)
    {
        if (m_phase == Client::Phase::Connecting)
        {
            m_host.reset(KickReason::ConnectionTimeout);
        }
        else if (m_phase == Client::Phase::Disconnecting)
        {
            Log::info(Format::format("Forced disconnection after no server "
                                     "response for {}s", timeout.count()));
            m_host.reset();
        }

        switchPhase(Client::Phase::Disconnected);
    }

    if (m_phase == Client::Phase::Connected)
        m_clientGame.update();
    else
        m_clientLobby.update(frameTime);
}

void Client::draw(float frameTime)
{
    if (m_phase != Client::Phase::Connected)
        m_clientLobby.draw();
    else
        m_clientGame.draw(frameTime);
}

bool Client::willExit() const
{
    return m_exited;
}

bool Client::connect(const Address &address)
{
    assert(m_phase == Client::Phase::Disconnected);

    if (m_host.connect(address))
    {
        //Notify the lobby
        m_clientLobby.switchToConnecting();

        //Start connecting
        switchPhase(Client::Phase::Connecting);
        m_lastHostRequestTime = Timer::getTime();
        return true;
    }

    m_clientLobby.switchToDisconnected(KickReason::EnetError);
    return false;
}

void Client::switchPhase(Client::Phase phase)
{
    m_host.setMessageReceiver((phase == Client::Phase::Connected) ?
                                &m_clientGame : nullptr);
    m_phase = phase;
}
