/*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 "Window.hpp"
#include "Structs/Vector.hpp"

#include "Tools/Log.hpp"
#include "Tools/Format.hpp"
#include "Graphics/GL.hpp"
#include "Structs/Color.hpp"
#include <SDL2/SDL_image.h>

namespace
{
    Uint32 getModeFlag(Display::DisplayMode mode)
    {
        if (mode == Display::DisplayMode::Fullscreen)
            return SDL_WINDOW_FULLSCREEN;
        else if (mode == Display::DisplayMode::FullscreenDesktop)
            return SDL_WINDOW_FULLSCREEN_DESKTOP;

        return 0;
    }

    Display::DisplayMode readModeFlag(SDL_Window *window)
    {
        Uint32 fullscreenFlag = SDL_GetWindowFlags(window) &
                                SDL_WINDOW_FULLSCREEN_DESKTOP;
        if (fullscreenFlag == SDL_WINDOW_FULLSCREEN_DESKTOP)
            return Display::DisplayMode::FullscreenDesktop;
        else if (fullscreenFlag == SDL_WINDOW_FULLSCREEN)
            return Display::DisplayMode::Fullscreen;

        return Display::DisplayMode::Windowed;
    }

    std::vector<SDL_DisplayMode> getSDLDisplayModes(SDL_Window *window)
    {
        const int displayIndex = SDL_GetWindowDisplayIndex(window);
        if (displayIndex < 0)
        {
            Log::err(Format::format("Could not retrieve display index: {}",
                                    SDL_GetError()));
            return {};
        }

        const int modesNo = SDL_GetNumDisplayModes(displayIndex);
        if (modesNo < 0)
        {
            Log::err(Format::format("Could not retrieve display modes: {}",
                                    SDL_GetError()));
            return {};
        }

        std::vector<SDL_DisplayMode> modes;
        modes.reserve(static_cast<std::size_t>(modesNo));

        for (int modeIndex = 0; modeIndex < modesNo; modeIndex++)
        {
            SDL_DisplayMode mode;
            if (SDL_GetDisplayMode(displayIndex, modeIndex, &mode) < 0)
            {
                Log::err(Format::format("Could not retrieve display mode "
                                        "{}: {}", modeIndex, SDL_GetError()));
                continue;
            }

            modes.push_back(mode);
        }

        return modes;
    }

    bool findSDLDisplayMode(SDL_Window *window, int width, int height,
                            SDL_DisplayMode &dst)
    {
        const std::vector<SDL_DisplayMode> modes = getSDLDisplayModes(window);
        for (const SDL_DisplayMode &mode : modes)
        {
            if (mode.w == width && mode.h == height)
            {
                dst = mode;
                return true;
            }
        }

        return false;
    }
}

Window::Window() : m_window(nullptr), m_glContext(nullptr),
m_latency(0.f), m_vsyncMode(Display::VSyncMode::Off)
{

}

bool Window::create(const char *title, const Display::DisplayLayout &layout,
                    Display::VSyncMode vsyncMode, int msBuffers, int msSamples)
{
    //Enable multisampling
    SDL_GL_SetAttribute(SDL_GL_MULTISAMPLEBUFFERS, msBuffers);
    SDL_GL_SetAttribute(SDL_GL_MULTISAMPLESAMPLES, msSamples);

    m_window = SDL_CreateWindow(title,
                                SDL_WINDOWPOS_UNDEFINED,
                                SDL_WINDOWPOS_UNDEFINED,
                                layout.width, layout.height,
                                SDL_WINDOW_RESIZABLE | SDL_WINDOW_OPENGL |
                                getModeFlag(layout.mode));

    if (m_window == nullptr)
    {
        Log::err(Format::format("Could not create window: {}", SDL_GetError()));
        if (msBuffers != 0 || msSamples != 0)
        {
            Log::info("Attempt to disable multisampling...");
            return create(title, layout, vsyncMode, 0, 0);
        }

        return false;
    }

    if (layout.mode == Display::DisplayMode::Fullscreen)
        applyFullscreenMode(layout.width, layout.height);

    //Create OpenGL Context
    m_glContext = SDL_GL_CreateContext(m_window);
    if (m_glContext == nullptr)
    {
        Log::err(Format::format("Could not create "
                                "OpenGL context: {}", SDL_GetError()));
        return false;
    }

    Log::info(Format::format("OpenGL {} ({})\n  Renderer: {}",
                             glGetString(GL_VERSION),
                             glGetString(GL_VENDOR),
                             glGetString(GL_RENDERER)));

    //Handle VSync
    setVSyncMode(vsyncMode);

    //GL parameters
    glPixelStorei(GL_UNPACK_ALIGNMENT, 1);

    return true;
}

void Window::destroy()
{
    SDL_GL_DeleteContext(m_glContext);
    SDL_DestroyWindow(m_window);
}

void Window::resizeWindow(const Display::DisplayLayout &layout)
{
    //Do not apply or modify real fullscreen modes after window creation
    //to avoid glitches (the change will be applied on next startup)
    if (layout.mode == Display::DisplayMode::Fullscreen ||
        readModeFlag(m_window) == Display::DisplayMode::Fullscreen)
        return;

    SDL_SetWindowFullscreen(m_window, getModeFlag(layout.mode));

    if (layout.mode == Display::DisplayMode::Windowed)
        SDL_SetWindowSize(m_window, layout.width, layout.height);

    initView(static_cast<float>(layout.width),
             static_cast<float>(layout.height));
}

void Window::initView(float viewWidth, float viewHeight)
{
    glMatrixMode(GL_PROJECTION);
    glLoadIdentity();

    //Init frame of reference (x: 0 (left) -> viewWidth (right),
    //                         y: 0 (top) -> viewHeight (bottom))
    glTranslatef(-1.f, 1.f, 0.f);
    glScalef(2.f / viewWidth, -2.f / viewHeight, 1.f);

    //Init viewport
    int sdl_windowWidth = 0, sdl_windowHeight = 0;
    SDL_GetWindowSize(m_window, &sdl_windowWidth, &sdl_windowHeight);

    float windowWidth = static_cast<float>(sdl_windowWidth),
          windowHeight = static_cast<float>(sdl_windowHeight);

    float windowRatio = windowWidth / windowHeight;
    float glRatio = viewWidth / viewHeight;

    Box sourceView;
    if (windowRatio > glRatio)
    {
        sourceView.width = viewWidth * (windowHeight / viewHeight);
        sourceView.height = windowHeight;

        sourceView.left = (windowWidth - sourceView.width) / 2.f;
        sourceView.top = 0.f;
    }
    else
    {
        sourceView.width = windowWidth;
        sourceView.height = viewHeight * (windowWidth / viewWidth);

        sourceView.left = 0.f;
        sourceView.top = (windowHeight - sourceView.height) / 2.f;
    }

    glViewport(static_cast<GLint>(sourceView.left),
               static_cast<GLint>(sourceView.top),
               static_cast<GLsizei>(sourceView.width),
               static_cast<GLsizei>(sourceView.height));

    m_viewport = Viewport(sourceView, Box(0.f, 0.f, viewWidth, viewHeight));
}

void Window::resizeView()
{
    initView(m_viewport.getTarget().width, m_viewport.getTarget().height);
}

void Window::clear()
{
    glClear(GL_COLOR_BUFFER_BIT);
}

void Window::flush()
{
    if (m_vsyncMode == Display::VSyncMode::Off)
    {
        m_time.waitUntilElapsed(Timer::duration_float_ms(m_latency));
        m_time.reset();
    }

    glFlush();
    SDL_GL_SwapWindow(m_window);
}

bool Window::pollEvent(SDL_Event &event)
{
    return SDL_PollEvent(&event);
}

//Get and set methods
void Window::setIcon(const std::string &iconPath)
{
    //Load surface
    SDL_Surface *surface = IMG_Load( iconPath.c_str() );
    if (!surface)
    {
        Log::err(Format::format("The icon file: {} could not be opened",
                                iconPath));
        return;
    }

    SDL_SetWindowIcon(m_window, surface);
    SDL_FreeSurface(surface);
}

void Window::setVSyncMode(Display::VSyncMode vsyncMode)
{
    m_vsyncMode = vsyncMode;

    if (m_vsyncMode == Display::VSyncMode::OnAdaptive &&
        SDL_GL_SetSwapInterval(-1) == -1)
    {
        Log::err(Format::format("Could not turn on adaptive vertical "
                                "synchronization: {}", SDL_GetError()));
        Log::info("Attempt to turn on regular vertical synchronization");
        m_vsyncMode = Display::VSyncMode::On;
    }

    if (m_vsyncMode == Display::VSyncMode::On &&
        SDL_GL_SetSwapInterval(1) == -1)
    {
        Log::err(Format::format("Could not turn on vertical "
                                "synchronization: {}", SDL_GetError()));
        Log::info("Attempt to turn off vertical synchronization");
        m_vsyncMode = Display::VSyncMode::Off;
    }

    if (m_vsyncMode == Display::VSyncMode::Off &&
        SDL_GL_SetSwapInterval(0) == -1)
    {
        Log::err(Format::format("Could not disable vertical "
                                "synchronization: {}", SDL_GetError()));
    }
}

void Window::setFrameRate(float frameRate)
{
    m_latency = (frameRate > 0.f) ? 1000.f / frameRate : 0.f;
}

Display::DisplayMode Window::getDisplayMode() const
{
    return readModeFlag(m_window);
}

const Viewport &Window::getViewport() const
{
    return m_viewport;
}

Vector Window::getViewSize() const
{
    return Vector(m_viewport.getTarget().width, m_viewport.getTarget().height);
}

std::vector<std::pair<int, int>> Window::getDisplayModeSizes()
{
    std::vector<SDL_DisplayMode> modes = getSDLDisplayModes(m_window);

    std::vector<std::pair<int, int>> modeSizes;
    modeSizes.reserve(modes.size());

    for (const SDL_DisplayMode &mode : modes)
    {
        if (modeSizes.empty() || modeSizes.back().first != mode.w ||
            modeSizes.back().second != mode.h)
        {
            modeSizes.push_back(std::make_pair(mode.w, mode.h));
        }
    }

    return modeSizes;
}

bool Window::applyFullscreenMode(int width, int height)
{
    SDL_DisplayMode sdlMode;
    if (!findSDLDisplayMode(m_window, width, height, sdlMode))
    {
        Log::err(Format::format("No fullscreen video mode available for "
                                "size {}x{}, using default video mode",
                                width, height));
        return false;
    }
    else if (SDL_SetWindowDisplayMode(m_window, &sdlMode) < 0)
    {
        Log::err(Format::format("Could not set video mode to fullscreen for"
                                " size {}x{}, using default video mode",
                                width, height));
        return false;
    }

    return true;
}
