/*
 * Copyright 2023 Hartmut Seichter
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 */
#include "paradiso/window.hpp"

#include "glad/glad.h"
#include "GLFW/glfw3.h"

#include <cmath>
#include <codecvt>
#include <locale>

#include <iostream>

namespace paradiso {

struct Window::impl {

    std::reference_wrapper<Window> parent_;

    GLFWwindow* window_ = nullptr;

    KeyboardInputStack keyboard_input_{};

    std::tuple<int, int> _old_size;
    std::tuple<int, int> _old_pos;

    //	window_context _context;

    static void error_callback(int error, const char* description) {
        std::cerr << "GLFW error: " << description << '\n';
    }

    static void drop_callback(GLFWwindow* window, int count,
                              const char** paths) {
        //        std::cout << __FUNCTION__ << std::endl;
        //		for (int i = 0; i < count; i++)
        //			std::cout << "\t" << paths[i] << std::endl;
    }

    static void scroll_callback(GLFWwindow* window, double xoffset,
                                double yoffset) {
        std::cout << __FUNCTION__ << std::endl;
    }

    static void mouse_button_callback(GLFWwindow* window, int button,
                                      int action, int mods) {
        //        input::get()._mouse_button = button;

        std::cout << __FUNCTION__ << " " << button << " " << action << " "
                  << mods << std::endl;
        //        input::get()._mouse_position
    }

    static void cursor_pos_callback(GLFWwindow* window, double xpos,
                                    double ypos) {
        //        input::get()._mouse_position =
        //        pointd(xpos,ypos).cast<float>();
    }

    static void key_callback(GLFWwindow* window, int key, int scancode,
                             int action, int mods) {
        Window::impl* impl =
            static_cast<Window::impl*>(glfwGetWindowUserPointer(window));

        impl->keyboard_input_.emplace(KeyboardInput{
            .key = key, .scancode = scancode, .action = action, .mods = mods});
    }

    static void charmods_callback(GLFWwindow* window, unsigned int codepoint,
                                  int mods) {
        // build the string from a Unicode code point
        std::wstring_convert<std::codecvt_utf8<char32_t>, char32_t> converter;
        std::string u8str = converter.to_bytes(codepoint);

        //        input::get()._input_string = u8str;
    }

    static void framebuffer_size_callback(GLFWwindow* window, int width,
                                          int height) {
        Window::impl* impl =
            static_cast<Window::impl*>(glfwGetWindowUserPointer(window));
        impl->parent_.get().on_resize_(impl->parent_);
    }

    impl(Window& w) : parent_(w) {
        // initialize
        if (!glfwInit()) {
            std::cerr << "Initalization error\n";
        }

        int glfw_major{}, glfx_minor{}, glfw_rev{};
        glfwGetVersion(&glfw_major, &glfx_minor, &glfw_rev);

        // we default to window invisible by default
        glfwWindowHint(GLFW_VISIBLE, GLFW_FALSE);

        // we do mostly 2D hence only request ancient OpenGL 4.0
        glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, 4);
        glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, 0);
        glfwWindowHint(GLFW_OPENGL_FORWARD_COMPAT, GL_TRUE);
        glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);

        //
        window_ = glfwCreateWindow(640, 480, "paradiso", nullptr, nullptr);

        // make window current
        glfwMakeContextCurrent(window_);

        // load opengl
        if (!gladLoadGLLoader((GLADloadproc)glfwGetProcAddress)) {
            std::cerr << "glad couldn't get OpenGL API";
        }

        // check Version
        int major{}, minor{}, rev{};
        major = glfwGetWindowAttrib(window_, GLFW_CONTEXT_VERSION_MAJOR);
        minor = glfwGetWindowAttrib(window_, GLFW_CONTEXT_VERSION_MINOR);
        rev = glfwGetWindowAttrib(window_, GLFW_CONTEXT_REVISION);

        // maybe something to pass to the outside
        //        sstt::core::Debug::d() << "OpenGL " << major << "." << minor
        //        << "." << rev;

        glfwSetWindowUserPointer(window_, this);

        glfwSetFramebufferSizeCallback(window_,
                                       Window::impl::framebuffer_size_callback);
        glfwSetKeyCallback(window_, Window::impl::key_callback);
        //        glfwSetCharCallback(_window, character_callback);
        glfwSetCharModsCallback(window_, charmods_callback);
        glfwSetScrollCallback(window_, scroll_callback);

        glfwSetDropCallback(window_, drop_callback);

        glfwSetCursorPosCallback(window_, cursor_pos_callback);
        glfwSetMouseButtonCallback(window_, mouse_button_callback);
        glfwSetScrollCallback(window_, scroll_callback);

        glfwSetErrorCallback(error_callback);

#if 0
        glEnable(GL_DEBUG_OUTPUT);
        glDebugMessageCallback(message_callback, 0);
#endif
        // glfwSetWindowCloseCallback(_window,close_callback);
    }

    ~impl() { glfwDestroyWindow(window_); }

    static void message_callback(GLenum source, GLenum type, GLuint id,
                                 GLenum severity, GLsizei length,
                                 const GLchar* message, const void* userParam) {
        fprintf(stderr,
                "GL CALLBACK: %s type = 0x%x, severity = 0x%x, message = %s\n",
                (type == GL_DEBUG_TYPE_ERROR ? "** GL ERROR **" : ""), type,
                severity, message);
    }

    bool update(Window::on_updatecallback_t cb) {
        if (window_ && !glfwWindowShouldClose(window_)) {

            // delete events
            paradiso::Window::KeyboardInputStack{}.swap(keyboard_input_);


            // get new events
            glfwPollEvents();

            if (cb(parent_)) {
                glfwSwapBuffers(window_);
                return true;
            }
        }

        return false;
    }

    void set_title(std::string_view title) {
        glfwSetWindowTitle(window_, title.data());
    }

    void set_size(const Size& size) {
        glfwSetWindowSize(window_, size.width, size.height);
    }

    Size size() const {
        int w{}, h{};
        glfwGetWindowSize(window_, &w, &h);
        return {.width = Size::value_type(w), .height = Size::value_type(h)};
    }

    Size client_size() const {
        int w{}, h{};
        glfwGetFramebufferSize(window_, &w, &h);
        return {.width = Size::value_type(w), .height = Size::value_type(h)};
    }

    Point position() const {
        int x{}, y{};
        glfwGetWindowPos(window_, &x, &y);
        return {.x = x, .y = y};
    }

    void set_position(const Point& p) { glfwSetWindowPos(window_, p.x, p.y); }

    void set_fullscreen(bool use_fullscreen) {
        if (fullscreen() == use_fullscreen)
            return;

        if (use_fullscreen) {

            auto& [x, y] = _old_pos;
            auto& [w, h] = _old_size;

            glfwGetWindowPos(window_, &x, &y);
            glfwGetWindowSize(window_, &w, &h);

            GLFWmonitor* monitor = glfwGetPrimaryMonitor();
            const GLFWvidmode* mode = glfwGetVideoMode(monitor);

            glfwSetWindowMonitor(window_, monitor, 0, 0, mode->width,
                                 mode->height, 0);
        } else {
            const auto& [x, y] = _old_pos;
            const auto& [w, h] = _old_size;

            glfwSetWindowMonitor(window_, nullptr, x, y, w, h, 0);
        }
    }

    bool fullscreen() const { return glfwGetWindowMonitor(window_) != nullptr; }

    void set_visible(bool show) {
        (show) ? glfwShowWindow(window_) : glfwHideWindow(window_);
    }

    bool visible() const {
        return glfwGetWindowAttrib(window_, GLFW_VISIBLE) > 0;
    }
};

//
//
//
Window::Window()
    : impl_(std::make_unique<Window::impl>(*this)), on_resize_([](Window&) {}) {
}

Window::~Window() {}

bool Window::update(Window::on_updatecallback_t cb) {
    return impl_->update(cb);
}

Size Window::size() const { return impl_->size(); }

Size Window::client_size() const { return impl_->client_size(); }

Point Window::position() const { return impl_->position(); }

bool Window::fullscreen() const { return impl_->fullscreen(); }

bool Window::visible() const { return impl_->visible(); }

Window& Window::set_visible(bool is_visible) {
    impl_->set_visible(is_visible);
    return *this;
}

Window& Window::set_fullscreen(bool use_fullscreen) {
    impl_->set_fullscreen(use_fullscreen);
    return *this;
}

Window& Window::set_position(const Point& position) {
    impl_->set_position(position);
    return *this;
}

Window& Window::set_size(const Size& size) {
    impl_->set_size(size);
    return *this;
}

Window& Window::set_title(std::string_view title) {
    impl_->set_title(title);
    return *this;
}
const Window::KeyboardInputStack& Window::keyboard_input() const {
    return impl_->keyboard_input_;
}


} // namespace paradiso
