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

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

#include <chrono>
#include <thread>

// TODO remove all hard coded 'magic' values!

// TODO - remove global variables
const int frame_rate = 60;
bool game_over = false;
float risky_pos_x;
float risky_pos_max_x;
float risky_pos_bottom_y;
float risky_pos_top_y;
float risky_pos_bottom_max_y;
float risky_pos_top_max_y;

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

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

    paradiso::Renderer renderer{};

    // TODO no constructors in rule of zero
    Background() {
        auto backgroundImage =
            paradiso::BitmapIO::get().load("background-day.png");
        backgroundLeft = paradiso::Sprite{
            .bitmap = backgroundImage,
            .pivot = {paradiso::Vector2<float>::make(0.0f, 0.16f)},
            .scale = {paradiso::Vector2<float>::make(1.01f, 1.3f)}};
        backgroundRight = paradiso::Sprite{
            .bitmap = backgroundImage,
            .pivot = {paradiso::Vector2<float>::make(2.018f, 0.16f)},
            .scale = {paradiso::Vector2<float>::make(1.01f, 1.3f)}};

        auto grassImage = paradiso::BitmapIO::get().load("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 (game_over == false) {
                if (sprite->pivot.x() <= -2.0f) {
                    sprite->pivot.x() += 4.0f;
                }
                sprite->pivot.x() -= 0.002f;
            }
            shader.set_uniform("pivot", sprite->pivot);
            shader.set_uniform("scale", sprite->scale);
            shader.set_uniform("rotation", sprite->rotation);
            renderer.draw(*sprite, shader);
        }
    }
};

struct Pipe {
    paradiso::Sprite pipe_top{};
    paradiso::Sprite pipe_bottom{};

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

    bool paused = false;

    int pipe_spawn_rand_int = rand() % 80 + 15;
    float pipe_spawn_rand = float(pipe_spawn_rand_int) / 100;

    bool pos_reset = false;

    // TODO no constructors in rule of zero

    Pipe() {
        auto pipe_image = paradiso::BitmapIO::get().load("pipe-green.png");

        pipe_top = paradiso::Sprite{
            .bitmap = pipe_image,
            .pivot = {paradiso::Vector2<float>::make(1.4f,
                                                     pipe_spawn_rand + 1.0f)},
            .scale = {paradiso::Vector2<float>::make(
                ((500.0f - (500.0f - 52.0f)) / 500.0f) * 2.25f,
                ((700.0f - (700.0f - 320.0f)) / 700.0f) * 2.25f)},
            .rotation = 3.1415926f};
        pipe_bottom = paradiso::Sprite{
            .bitmap = pipe_image,
            .pivot = {paradiso::Vector2<float>::make(1.4f,
                                                     pipe_spawn_rand - 1.5f)},
            .scale = {paradiso::Vector2<float>::make(
                ((500.0f - (500.0f - 52.0f)) / 500.0f) * 2.25f,
                ((700.0f - (700.0f - 320.0f)) / 700.0f) * 2.25f)}};

        paused = true;
    }

    void update() {

        // TODO improve state handling

        if (game_over == true) {
            paused = true;
        }

        if (paused == true) {
            return;
        } else {
            pipe_spawn_rand_int = rand() % 80 + 15;
            pipe_spawn_rand = float(pipe_spawn_rand_int) / 100;

            pipe_top.pivot.x() -= 0.02f;
            pipe_bottom.pivot.x() -= 0.02f;

            risky_pos_x = pipe_top.pivot.x();
            risky_pos_max_x = pipe_top.pivot.x() +
                              ((500.0f - (500.0f - 52.0f)) / 500.0f) * 2.25f;

            risky_pos_top_y = pipe_top.pivot.y();
            risky_pos_top_max_y = pipe_top.pivot.y() + 10.0f;

            risky_pos_bottom_y = pipe_bottom.pivot.y();
            risky_pos_bottom_max_y = pipe_bottom.pivot.y() - 10.0f;

            if (pipe_top.pivot.x() <= -1.4f || pipe_bottom.pivot.x() <= -1.4f) {
                pos_reset = true;

                if (pos_reset == true) {
                    pipe_top.pivot.y() = pipe_spawn_rand + 1.0f;
                    pipe_bottom.pivot.y() = pipe_spawn_rand - 1.5;

                    pos_reset = false;
                }

                pipe_top.pivot.x() = 1.4f;
                pipe_bottom.pivot.x() = 1.4f;
            }
        }
    }

    void draw(const paradiso::Shader& shader) {
        shader.set_uniform("pivot", pipe_bottom.pivot);
        shader.set_uniform("scale", pipe_bottom.scale);
        renderer1.draw(pipe_bottom, shader);

        shader.set_uniform("pivot", pipe_top.pivot);
        shader.set_uniform("scale", pipe_top.scale);
        shader.set_uniform("rotation", pipe_top.rotation);
        renderer1.draw(pipe_top, shader);
    }
};

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

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

    // TODO no constructors in rule of zero
    Grass() {
        auto grassImage = paradiso::BitmapIO::get().load("base.png");
        grassLeft = paradiso::Sprite{
            .bitmap = grassImage,
            .pivot = {paradiso::Vector2<float>::make(0.0f, -0.9f)},
            .scale = {paradiso::Vector2<float>::make(
                ((500.0f - (500.0f - 504.0f)) / 500.0f) * 2.25f,
                ((700.0f - (700.0f - 112.0f)) / 700.0f) * 2.25f)}};
        grassRight = paradiso::Sprite{
            .bitmap = grassImage,
            .pivot = {paradiso::Vector2<float>::make(1.002f, -0.9f)},
            .scale = {paradiso::Vector2<float>::make(
                ((500.0f - (500.0f - 504.0f)) / 500.0f) * 2.25f,
                ((700.0f - (700.0f - 112.0f)) / 700.0f) * 2.25f)}};
    }

    void draw(const paradiso::Shader& shader) {
        if (game_over == false) {
            grassLeft.pivot.x() -= 0.02f;
            grassRight.pivot.x() -= 0.02f;

            if (grassRight.pivot.x() <= 0.0f) {
                grassLeft.pivot.x() = 1.002f;
            }
            if (grassRight.pivot.x() <= -1.002f) {
                grassRight.pivot.x() = 1.002f;
            }
        }

        shader.set_uniform("pivot", grassLeft.pivot);
        shader.set_uniform("scale", grassLeft.scale);
        shader.set_uniform("rotation", grassLeft.rotation);

        shader.set_uniform("pivot", grassRight.pivot);
        shader.set_uniform("scale", grassRight.scale);
        shader.set_uniform("rotation", grassRight.rotation);

        renderer1.draw(grassLeft, shader);
        renderer2.draw(grassRight, shader);
    }
};

struct QuickWings {
    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.02f;

    const float gravity = -0.002f;
    const float move_up_velocity = 0.02f;

    bool move_up = false;
    bool paused = true;

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

    float pos = 0.34f;

    float rotation = 0.0f;

    int collision_counter = 0;

    // TODO no constructors in rule of zero
    QuickWings() {
        float scaleh = 0.08f;
        float scalew = 0.158f;

        birds = {
            paradiso::Sprite{
                .bitmap =
                    paradiso::BitmapIO::get().load("yellowbird-downflap.png"),
                .pivot = {paradiso::Vector2<float>::make(0.0f, 0.0f)},
                .scale = {paradiso::Vector2<float>::make(scalew, scaleh)}},
            paradiso::Sprite{
                .bitmap =
                    paradiso::BitmapIO::get().load("yellowbird-midflap.png"),
                .pivot = {paradiso::Vector2<float>::make(0.0f, 0.0f)},
                .scale = {paradiso::Vector2<float>::make(scalew, scaleh)}},
            paradiso::Sprite{
                .bitmap =
                    paradiso::BitmapIO::get().load("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() {

        if (game_over == true) {
            paused = true;
        }

        // Stop game
        if (paused) {
            return;
        } else {
            // 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;
                game_over = true;
            }
            if (pos > max_pos) {
                pos = max_pos;
                velocity = 0.0f;
                game_over = true;
            }

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

        float final_risky_pos_top_y = risky_pos_top_y - 1.06f;
        float final_risky_pos_bottom_y = risky_pos_bottom_y + 1.06f;

        if (risky_pos_x - 0.3f <= 0.0f && risky_pos_max_x >= 0.0f) {
            if (pos >= final_risky_pos_top_y && pos <= risky_pos_top_max_y) {
                game_over = true;
            }
            if (pos <= final_risky_pos_bottom_y &&
                pos >= risky_pos_bottom_max_y) {
                if (collision_counter == 0) {
                    collision_counter++;
                } else {
                    collision_counter = 2;
                }

                if (collision_counter == 2) {
                    game_over = true;
                }
            }
        }
    }

    // keyboard handler
    void on_keyboard(const paradiso::Window::KeyboardInputStack& input) {
        if (input.size()) {
            paused = false;

            if (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;
                }
            } else {
                return;
            }
        }
    }
};

struct Message {
    paradiso::Sprite messageSprite;
    paradiso::Renderer renderer{};
    bool start = false;
    float pos = 100.0f;

    Message() {
        auto messageImage = paradiso::BitmapIO::get().load("message.png");
        messageSprite = paradiso::Sprite{
            .bitmap = messageImage,
            .pivot = {paradiso::Vector2<float>::make(0.0f, 0.0f)},
            .scale = {paradiso::Vector2<float>::make(0.8f, 0.8f)}};
    };

    void draw(const paradiso::Shader& shader) {
        shader.set_uniform("pivot", messageSprite.pivot);
        shader.set_uniform("scale", messageSprite.scale);
        renderer.draw(messageSprite, shader);
    }

    void update() {
        if (start == true) {
            messageSprite.pivot.y() = pos;
        }
    }

    void on_keyboard(const paradiso::Window::KeyboardInputStack& input) {
        if (input.size()) {
            start = true;
        }
    }
};

struct GameOverMessage {
    paradiso::Sprite messageSprite;
    paradiso::Renderer renderer{};

    GameOverMessage() {
        auto messageImage = paradiso::BitmapIO::get().load("gameover.png");
        messageSprite = paradiso::Sprite{
            .bitmap = messageImage,
            .pivot = {paradiso::Vector2<float>::make(0.0f, 0.4f)},
            .scale = {paradiso::Vector2<float>::make(
                ((500.0f - (500.0f - 192.0f)) / 500.0f) * 2.25f,
                ((700.0f - (700.0f - 42.0f)) / 700.0f) * 2.25f)}};
    };

    void draw(const paradiso::Shader& shader) {
        shader.set_uniform("pivot", messageSprite.pivot);
        shader.set_uniform("scale", messageSprite.scale);
        renderer.draw(messageSprite, shader);
    }
};

auto main() -> int {
    std::srand(std::time(nullptr));

    // 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 = 1920 / 2 - 500 / 2,
                                      .y = 1080 / 2 - 700 / 2}) // ... 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));

    // Asset loader bekommt den Pfad

    paradiso::BitmapIO::get().set_path("assets");

    // Load
    auto background = Background{};
    auto grass = Grass{};
    auto quickwingsapp = QuickWings{};
    auto pipe = Pipe{};
    auto message = Message{};
    auto gameover = GameOverMessage{};

    // 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 + update
        if (quickwingsapp.paused == false) {
            pipe.paused = false;
        }

        pipe.update();

        quickwingsapp.on_keyboard(w.keyboard_input());
        quickwingsapp.update();

        message.on_keyboard(w.keyboard_input());
        message.update();

        // Draw
        background.draw(shader);
        pipe.draw(shader);
        grass.draw(shader);
        message.draw(shader);

        if (game_over == true) {
            gameover.draw(shader);
        }

        quickwingsapp.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;
}
