/**
 * paradiso - Paradigmen der Softwareentwicklung
 *
 * (c) Copyright 2023 Hartmut Seichter
 *
 */

#include <paradiso/bitmap.hpp>
#include <paradiso/context.hpp>
#include <paradiso/geometry.hpp>
#include <paradiso/renderer.hpp>
#include <paradiso/shader.hpp>
#include <paradiso/sprite.hpp>
#include <paradiso/vector.hpp>
#include <paradiso/window.hpp>

#include <chrono>
#include <iomanip>
#include <iostream>
#include <unordered_map>

#include "lib/image_loader.hpp"

const int frame_rate = 60;

struct Background {
    paradiso::Sprite backgroundLeft;
    paradiso::Sprite backgroundRight;
    paradiso::Sprite grassLeft;
    paradiso::Sprite grassRight;

    paradiso::Sprite* scrolling[2] = {&backgroundLeft, &backgroundRight};

    Background() {
        auto backgroundImage =
            image_loader::load(std::string("background-day.png"));
        backgroundLeft = paradiso::Sprite{
            .bitmap = backgroundImage,
            .pivot = paradiso::Vector2<float>::make(0.0f, 0.0f),
            .scale = paradiso::Vector2<float>::make(1.01f, 1.0f)};
        backgroundRight = paradiso::Sprite{
            .bitmap = backgroundImage,
            .pivot = paradiso::Vector2<float>::make(2.0f, 0.0f),
            .scale = paradiso::Vector2<float>::make(1.01f, 1.0f)};

        auto grassImage = image_loader::load(std::string("base.png"));
        grassLeft = paradiso::Sprite{
            .bitmap = grassImage,
            .pivot = paradiso::Vector2<float>::make(0.0f, -1.0f),
            .scale = paradiso::Vector2<float>::make(1.0f, 0.33333f)};
        grassRight = paradiso::Sprite{
            .bitmap = grassImage,
            .pivot = paradiso::Vector2<float>::make(2.0f, -1.0f),
            .scale = paradiso::Vector2<float>::make(1.0f, 0.33333f)};
    }

    void draw(const paradiso::Shader& shader) {
        for (auto sprite : scrolling) {
            if (sprite->pivot.x() <= -2.0f) {
                sprite->pivot.x() += 4.0f;
            }
            sprite->pivot.x() -= 0.001f;
            shader.set_uniform("pivot", sprite->pivot);
            shader.set_uniform("scale", sprite->scale);
            shader.set_uniform("rotation", sprite->rotation);
            renderer.draw(*sprite, shader);
        }
    }

    paradiso::Renderer renderer{};
};

struct Grass {
    paradiso::Sprite grassLeft;
    paradiso::Sprite grassRight;

    paradiso::Sprite* scrolling[2] = {&grassLeft, &grassRight};

    Grass() {
        auto grassImage = image_loader::load(std::string("base.png"));
        grassLeft = paradiso::Sprite{
            .bitmap = grassImage,
            .pivot = paradiso::Vector2<float>::make(0.0f, -0.9f),
            .scale = paradiso::Vector2<float>::make(1.0f, 0.33333f)};
        grassRight = paradiso::Sprite{
            .bitmap = grassImage,
            .pivot = paradiso::Vector2<float>::make(2.0f, -0.9f),
            .scale = paradiso::Vector2<float>::make(1.0f, 0.33333f)};
    }

    void draw(const paradiso::Shader& shader) {
        for (auto sprite : scrolling) {
            if (sprite->pivot.x() <= -2.0f) {
                sprite->pivot.x() += 4.0f;
            }
            sprite->pivot.x() -= 0.035f;
            shader.set_uniform("pivot", sprite->pivot);
            shader.set_uniform("scale", sprite->scale);
            shader.set_uniform("rotation", sprite->rotation);
            renderer.draw(*sprite, shader);
        }
    }

    paradiso::Renderer renderer{};
};

struct FlappyBird {

    paradiso::Renderer renderer1{};
    paradiso::Renderer renderer2{};
    paradiso::Renderer renderer3{};

    std::array<paradiso::Sprite, 3> birds;
    unsigned int flap = 0;
    unsigned int flapSpeed = 15;  // How many ticks per flap
    unsigned int flapCounter = 0; // How many ticks since last flap

    float velocity = 0.0f;
    const float max_velocity = 0.05f;

    const float gravity = -0.004f;
    const float move_up_velocity = 0.0055f;

    bool move_up = false;
    bool paused = true;

    const float max_pos = 0.95f;
    const float min_pos = -0.5f;

    float pos = 0.0f;

    float rotation = 0.0f;

    FlappyBird() {
        float scaleh = 0.07f;
        float scalew = scaleh * 1.416666666666667f;

        birds = {
            paradiso::Sprite{
                .bitmap =
                    image_loader::load(std::string("yellowbird-downflap.png")),
                .pivot = paradiso::Vector2<float>::make(0.0f, 0.0f),
                .scale = paradiso::Vector2<float>::make(scalew, scaleh)},
            paradiso::Sprite{
                .bitmap =
                    image_loader::load(std::string("yellowbird-midflap.png")),
                .pivot = paradiso::Vector2<float>::make(0.0f, 0.0f),
                .scale = paradiso::Vector2<float>::make(scalew, scaleh)},
            paradiso::Sprite{
                .bitmap =
                    image_loader::load(std::string("yellowbird-upflap.png")),
                .pivot = paradiso::Vector2<float>::make(0.0f, 0.0f),
                .scale = paradiso::Vector2<float>::make(scalew, scaleh)}};
    }

    void draw(const paradiso::Shader& shader) {
        // Update flap state
        if (flapCounter < flapSpeed) {
            flapCounter++;
        } else {
            flapCounter = 0;
            flap = (flap + 1) % birds.size();
        }

        auto bird = birds[flap];
        bird.pivot.y() = pos;
        bird.rotation = rotation;
        shader.set_uniform("pivot", bird.pivot);
        shader.set_uniform("scale", bird.scale);
        shader.set_uniform("rotation", bird.rotation);

        switch (flap) {
        case 0:
            renderer1.draw(bird, shader);
            break;
        case 1:
            renderer2.draw(bird, shader);
            break;
        case 2:
            renderer3.draw(bird, shader);
            break;
        }
    }

    void update() {
        // Stop game
        if (paused)
            return;

        // Apply gravity
        velocity += gravity;

        if (move_up)
            velocity += move_up_velocity - gravity;

        // Cap velocity
        if (velocity > max_velocity)
            velocity = max_velocity;
        if (velocity < -max_velocity)
            velocity = -max_velocity;

        // Cap position
        pos += velocity;
        if (pos < min_pos) {
            pos = min_pos;
            velocity = 0.0f;
        }
        if (pos > max_pos) {
            pos = max_pos;
            velocity = 0.0f;
        }

        // Update rotation
        rotation = velocity * 15.0f;
    }

    // keyboard handler
    void on_keyboard(const paradiso::Window::KeyboardInputStack& input) {
        if (input.size()) {
            paused = false;
            bool pressed_up = input.top().key == ' ' || input.top().key == 'W';

            if (input.top().action == 1 && pressed_up) {
                move_up = true;
            } else if (input.top().action == 0 && pressed_up) {
                move_up = false;
            }
        }
    }
};

auto main() -> int {

    // Ausgabefenster ... sieht aus als wäre es auf dem Stack
    auto window = paradiso::Window();

    auto size = paradiso::Size{.width = 500, .height = 700};

    /*
    window.set_resizecallback([](auto& w) -> void {
        w.set_size(paradiso::Size{.width = 405, .height = 720});
    });

*/
    window
        .set_size(size)                                    // ... Grösse
        .set_position(paradiso::Point{.x = 100, .y = 100}) // ... Position
        .set_title("PardiSO.FlappyBird")                   // ... Titel
        .set_visible(true); // ... und jetzt anzeigen!

    // der Fenster Kontext
    auto ctx = paradiso::Context{};

    // ein Shader (Schattierungsprogramm)
    auto shader = paradiso::Shader{};

    // wir nutzen einen vorgefertigten shader
    shader.load_preset(paradiso::Shader::Preset::Sprite);

    // ein viewport stellt die Sicht der Kamera dar, d.h. bei quadratischen
    // Pixeln sollte hier auch eine dementsprechende Grösse eingestellt
    // werden
    ctx.set_viewport(paradiso::Rectangle{
        .position = paradiso::Point{.x = 0, .y = 0}, .size = size});

    // nothing beats a classic look
    ctx.set_clearcolor(paradiso::RGBA::from_rgb(0x00, 0x00, 0x00));

    // Load
    auto background = Background{};
    auto grass = Grass{};

    auto flappyBird = FlappyBird{};

    // timer

    // das update führt den hier mitgegebnen Ausdruck innerhalb der internen
    // Updates des Fensters auf. Es wird hier auch explizit ein bool gefordert
    // damit das update auch zu jederzeit unterbrochen werden kann
    while (window.update([&](paradiso::Window& w) -> bool {
        ctx.set_viewport(paradiso::Rectangle{
            .position = paradiso::Point{.x = 0, .y = 0}, .size = size});

        ctx.clear();
        auto t1 = std::chrono::high_resolution_clock::now();

        // Keyboard and state change
        flappyBird.on_keyboard(w.keyboard_input());
        flappyBird.update();

        // Draw
        background.draw(shader);
        grass.draw(shader);
        flappyBird.draw(shader);

        // wait for frame rate
        auto t2 = std::chrono::high_resolution_clock::now();
        auto duration =
            std::chrono::duration_cast<std::chrono::microseconds>(t2 - t1);
        auto wait = std::chrono::microseconds(1000000 / frame_rate) - duration;
        std::this_thread::sleep_for(wait);

        // Quit
        return !(w.keyboard_input().size() &&
                 w.keyboard_input().top().key == 'Q');
    })) {
    };

    return 0;
}
