#include "rendering/Window.h"

#include <utility>

#include "GL/glew.h"
#include "GLFW/glfw3.h"
#include "data/Array.h"
#include "data/HashMap.h"

static GLFWwindow* window = nullptr;
static Clock fps;
static Clock tps;
static IntVector2 size{0, 0};
static bool sizeChanged = false;

static List<uint32> input;
static int inputCursor = 0;
static int inputLimit = 256;
static bool inputActive = false;

struct Button final {
    Window::Controls::ButtonName name;
    int key;
    int downTime;
    int upEvents;
    int downEvents;
    int controllerUp;
    int controllerDown;
    bool released;

    Button(const Window::Controls::ButtonName& name_)
        : name(name_), key(0), downTime(0), upEvents(0), downEvents(0),
          controllerUp(0), controllerDown(0), released(false) {
    }

    void tick() {
        bool down = (downEvents > 0) || (controllerDown > 0);
        bool up = (upEvents == downEvents) && (controllerUp == controllerDown);

        if(released) {
            downTime = 0;
        }
        downTime += down;
        released = down && up;

        downEvents -= upEvents;
        upEvents = 0;

        controllerDown -= controllerUp;
        controllerUp = 0;
    }
};
static Button fallbackButton{"unknown"};
static List<Button> buttons;
static HashMap<int, Window::Controls::ButtonId> keyToButtonId;
static HashMap<int, Window::Controls::ButtonId> gamepadToButtonId;
static HashMap<int, Window::Controls::ButtonId> mouseToButtonId;
static Vector2 lastMousePosition;
static Vector2 mousePosition;
static int activeController = -1;
static GLFWgamepadstate lastControllerState;

Window::Options::Options(int majorVersion_, int minorVersion_,
                         const IntVector2& size_, bool es_, const char* name_)
    : majorVersion(majorVersion_), minorVersion(minorVersion_), size(size_),
      fullscreen(false), es(es_), vsync(true), name(name_) {
}

static void onButton(HashMap<int, Window::Controls::ButtonId>& map, int key,
                     int action) {
    Window::Controls::ButtonId* b = map.search(key);
    if(b == nullptr) {
        return;
    }
    Window::Controls::ButtonId id = *b;
    if(id < 0 || id >= buttons.getLength()) {
        return;
    }
    if(action == GLFW_RELEASE) {
        buttons[id].upEvents++;
    } else if(action == GLFW_PRESS) {
        buttons[id].downEvents++;
    }
}

static void addError(Error& e) {
    const char* description = nullptr;
    int errorCode = glfwGetError(&description);
    if(errorCode == GLFW_NO_ERROR) {
        return;
    }
    e.message.append(": ").append(description);
}

static void handleInputKey(int key, int action) {
    if(action == GLFW_RELEASE) {
        return;
    }
    switch(key) {
        case GLFW_KEY_BACKSPACE:
            if(input.getLength() > inputCursor - 1 && inputCursor > 0) {
                input.remove(inputCursor - 1);
                inputCursor--;
            }
            break;
        case GLFW_KEY_LEFT: inputCursor -= inputCursor > 0; break;
        case GLFW_KEY_RIGHT:
            inputCursor += inputCursor < input.getLength();
            break;
    }
}

static void onKey(GLFWwindow*, int key, int scancode, int action, int mods) {
    (void)scancode;
    (void)mods;
    if(inputActive) {
        handleInputKey(key, action);
    }
    onButton(keyToButtonId, key, action);
}

static void addUnicode(uint32 codepoint) {
    if(input.getLength() >= inputLimit) {
        return;
    }
    input.add(codepoint);
    for(int i = input.getLength() - 1; i > inputCursor; i--) {
        std::swap(input[i], input[i - 1]);
    }
    inputCursor++;
}

static void onChar(GLFWwindow*, uint32 codepoint) {
    if(inputActive) {
        addUnicode(codepoint);
    }
}

static void onResize(GLFWwindow*, int width, int height) {
    sizeChanged = true;
    size[0] = width;
    size[1] = height;
}

static void onMouse(GLFWwindow*, int button, int action, int mods) {
    (void)mods;
    onButton(mouseToButtonId, button, action);
}

static void onMouseMove(GLFWwindow*, double x, double y) {
    mousePosition = Vector<2, double>(x, y).toFloat();
}

Error Window::open(const Options& o) {
    if(!glfwInit()) {
        Error e{"could not initialize GLFW"};
        addError(e);
        return e;
    }

    glfwDefaultWindowHints();
    glfwWindowHint(GLFW_VISIBLE, false);
    glfwWindowHint(GLFW_RESIZABLE, true);
    glfwWindowHint(GLFW_DECORATED, !o.fullscreen);
    glfwWindowHint(GLFW_DOUBLEBUFFER, true);

    if(o.es) {
        glfwWindowHint(GLFW_CLIENT_API, GLFW_OPENGL_ES_API);
    } else {
        glfwWindowHint(GLFW_OPENGL_PROFILE, GLFW_OPENGL_CORE_PROFILE);
    }
    glfwWindowHint(GLFW_CONTEXT_VERSION_MAJOR, o.majorVersion);
    glfwWindowHint(GLFW_CONTEXT_VERSION_MINOR, o.minorVersion);

    GLFWmonitor* m = o.fullscreen ? glfwGetPrimaryMonitor() : nullptr;
    window = glfwCreateWindow(o.size[0], o.size[1], o.name, m, nullptr);
    if(window == nullptr) {
        Error e{"could not create window"};
        addError(e);
        close();
        return e;
    }
    size = o.size;
    glfwSetKeyCallback(window, onKey);
    glfwSetCharCallback(window, onChar);
    glfwSetFramebufferSizeCallback(window, onResize);
    glfwSetMouseButtonCallback(window, onMouse);
    glfwSetCursorPosCallback(window, onMouseMove);

    glfwMakeContextCurrent(window);
    glfwSwapInterval(o.vsync);

    GLenum err = glewInit();
    if(err != GLEW_OK) {
        Error e{"could not initialize GLEW: "};
        e.message.append(glewGetErrorString(err));
        close();
        return e;
    }
    return {};
}

void Window::close() {
    if(window != nullptr) {
        glfwDestroyWindow(window);
        window = nullptr;
    }
    glfwTerminate();
}

float Window::getTicksPerSecond() {
    return tps.getUpdatesPerSecond();
}

float Window::getFramesPerSecond() {
    return fps.getUpdatesPerSecond();
}

const IntVector2& Window::getSize() {
    return size;
}

bool Window::hasSizeChanged() {
    return sizeChanged;
}

void Window::trapCursor() {
    glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_DISABLED);
}

void Window::freeCursor() {
    glfwSetInputMode(window, GLFW_CURSOR, GLFW_CURSOR_NORMAL);
}

bool Window::isCursorTrapped() {
    return glfwGetInputMode(window, GLFW_CURSOR) == GLFW_CURSOR_DISABLED;
}

void Window::show() {
    glfwShowWindow(window);
}

bool Window::shouldClose() {
    return glfwWindowShouldClose(window);
}

Clock::Nanos Window::startFrame() {
    return fps.update();
}

static bool searchForGamepad() {
    if(activeController != -1) {
        return true;
    }
    for(int i = GLFW_JOYSTICK_1; i <= GLFW_JOYSTICK_LAST; i++) {
        if(glfwJoystickIsGamepad(i)) {
            activeController = i;
            return true;
        }
    }
    return false;
}

static void checkGamepad() {
    GLFWgamepadstate state;
    if(!glfwGetGamepadState(activeController, &state)) {
        activeController = -1;
        return;
    }
    for(int i = 0; i <= GLFW_GAMEPAD_BUTTON_LAST; i++) {
        Window::Controls::ButtonId* idp = gamepadToButtonId.search(i);
        if(idp == nullptr) {
            continue;
        }
        Window::Controls::ButtonId id = *idp;
        if(id < 0 || id >= buttons.getLength()) {
            continue;
        }
        if(!lastControllerState.buttons[i] && state.buttons[i]) {
            buttons[id].controllerDown++;
        } else if(lastControllerState.buttons[i] && !state.buttons[i]) {
            buttons[id].controllerUp++;
        }
    }
    lastControllerState = state;
}

void Window::endFrame() {
    glfwSwapBuffers(window);
    sizeChanged = false;
    glfwPollEvents();
    if(searchForGamepad()) {
        checkGamepad();
    }
}

void Window::tick() {
    tps.update();
    for(Button& b : buttons) {
        b.tick();
    }
}

void Window::postTick() {
    lastMousePosition = mousePosition;
}

void Window::Input::setLimit(int l) {
    inputLimit = l;
    while(input.getLength() > inputLimit) {
        input.removeBySwap(inputLimit);
    }
}

void Window::Input::reset() {
    input.clear();
    inputCursor = 0;
}

void Window::Input::enable() {
    inputActive = true;
}

void Window::Input::disable() {
    inputActive = false;
}

bool Window::Input::isEnabled() {
    return inputActive;
}

static uint32 read(int& index, const char* s) {
    if(s[index] == '\0') {
        return '\0';
    }
    return static_cast<uint32>(s[index++]);
}

static uint32 readUnicode(int& index, const char* s) {
    uint32 c = read(index, s);
    if((c & 0xE0) == 0xC0) {
        c = ((c & 0x1F) << 6) | (read(index, s) & 0x3F);
    } else if((c & 0xF0) == 0xE0) {
        c = ((c & 0xF) << 12) | ((read(index, s) & 0x3F) << 6);
        c |= read(index, s) & 0x3F;
    } else if((c & 0xF8) == 0xF0) {
        c = ((c & 0x7) << 18) | ((read(index, s) & 0x3F) << 12);
        c |= (read(index, s) & 0x3F) << 6;
        c |= read(index, s) & 0x3F;
    }
    return c;
}

void Window::Input::fill(const char* s) {
    int index = 0;
    reset();
    while(true) {
        uint32 c = readUnicode(index, s);
        if(c == '\0') {
            break;
        }
        addUnicode(c);
    }
}

int Window::Input::getCursor() {
    return inputCursor;
}

void Window::Input::setCursor(int index) {
    if(index < 0) {
        inputCursor = 0;
    } else if(index > input.getLength()) {
        inputCursor = input.getLength();
    } else {
        inputCursor = index;
    }
}

const List<uint32>& Window::Input::getUnicode() {
    return input;
}

Window::Controls::ButtonId Window::Controls::add(const ButtonName& name) {
    ButtonId id = buttons.getLength();
    buttons.add(name);
    return id;
}

void Window::Controls::bindKey(ButtonId id, int key) {
    keyToButtonId.add(key, id);
}

void Window::Controls::bindGamepad(ButtonId id, int gamepadButton) {
    gamepadToButtonId.add(gamepadButton, id);
}

void Window::Controls::bindMouse(ButtonId id, int mouseButton) {
    mouseToButtonId.add(mouseButton, id);
}

Vector2 Window::Controls::getLastMousePosition() {
    return lastMousePosition;
}

Vector2 Window::Controls::getMousePosition() {
    return mousePosition;
}

Vector2 Window::Controls::getLeftGamepadAxis() {
    return Vector2(lastControllerState.axes[GLFW_GAMEPAD_AXIS_LEFT_X],
                   lastControllerState.axes[GLFW_GAMEPAD_AXIS_LEFT_Y]);
}

Vector2 Window::Controls::getRightGamepadAxis() {
    return Vector2(lastControllerState.axes[GLFW_GAMEPAD_AXIS_RIGHT_X],
                   lastControllerState.axes[GLFW_GAMEPAD_AXIS_RIGHT_Y]);
}

float Window::Controls::getLeftGamepadTrigger() {
    return lastControllerState.axes[GLFW_GAMEPAD_AXIS_LEFT_TRIGGER];
}

float Window::Controls::getRightGamepadTrigger() {
    return lastControllerState.axes[GLFW_GAMEPAD_AXIS_RIGHT_TRIGGER];
}

static const Button& getButton(Window::Controls::ButtonId id) {
    if(id < 0 || id >= buttons.getLength()) {
        return fallbackButton;
    }
    return buttons[id];
}

bool Window::Controls::isDown(ButtonId id) {
    return getButton(id).downTime > 0;
}

int Window::Controls::getDownTime(ButtonId id) {
    return getButton(id).downTime;
}

bool Window::Controls::wasReleased(ButtonId id) {
    return getButton(id).released;
}

const Window::Controls::ButtonName& Window::Controls::getName(ButtonId id) {
    return getButton(id).name;
}