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

#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 <unordered_map>

#include <iostream>

struct PongStage {

    using Vec2 = paradiso::Vector2<float>;

    Vec2 tl{Vec2::make(-1.0f, -1.0f)};
    Vec2 br{Vec2::make(+1.0f, +1.0f)};

    enum class TouchPoint { None, Left, Right, Bottom, Top };

    TouchPoint touch(const Vec2& o, float eps = 0.001f) noexcept {
        std::unordered_map<TouchPoint, float> deltas;
        deltas[TouchPoint::Left] = std::abs(o.x() - tl.x());
        deltas[TouchPoint::Right] = std::abs(o.x() - br.x());
        deltas[TouchPoint::Bottom] = std::abs(o.y() - br.y());
        deltas[TouchPoint::Top] = std::abs(o.y() - tl.y());

        auto min_el = *std::min_element(
            std::begin(deltas), std::end(deltas),
            [](const auto& l, const auto& r) { return l.second < r.second; });

        auto t = (min_el.second <= eps) ? min_el.first : TouchPoint::None;

        return t;
    };

    // sprite
    paradiso::Sprite sprite{
        .bitmap = paradiso::Bitmap::from_data(
            paradiso::Size{1, 1}, paradiso::RGBA::from_rgb(0x80, 0xFF, 0xFF))};

    void draw(const paradiso::Shader& shader) { renderer.draw(sprite, shader); }

    paradiso::Renderer renderer{};
};

struct PongPaddle {

    static constexpr float whoopiness = 0.95f;

    // sprite
    paradiso::Sprite sprite{
        .bitmap = paradiso::Bitmap::from_data(paradiso::Size{1, 1},
                                              paradiso::RGBA::white()),
        .pivot = paradiso::Vector2<float>::make(0.0f, -0.9f),
        .scale = paradiso::Vector2<float>::make(0.25f, 0.01f)};

    // keyboard handler
    void on_keyboard(const paradiso::Window::KeyboardInputStack& input) {

        if (input.size()) {
            if (input.top().key == 'A' || input.top().key == 263) {
                velocity_horizontal -= 0.01;
            } else if (input.top().key == 'D' || input.top().key == 262) {
                velocity_horizontal += 0.01;
            }
        }
    }

    void draw(const paradiso::Shader& shader) {

        // update internal state
        velocity_horizontal *= whoopiness;
        sprite.pivot.x() += velocity_horizontal;

        sprite.pivot.x() = std::clamp(sprite.pivot.x(), -0.5f, 0.5f);

        // update shader uniforms
        shader.set_uniform("pivot", sprite.pivot);
        shader.set_uniform("scale", sprite.scale);
        shader.set_uniform("rotation", sprite.rotation);

        renderer.draw(sprite, shader);
    }

    float velocity_horizontal{};

    paradiso::Renderer renderer{};
};

struct PongBall {
    // sprite
    paradiso::Sprite sprite{
        .bitmap = paradiso::Bitmap::from_data(paradiso::Size{1, 1},
                                              paradiso::RGBA::white()),
        .pivot = paradiso::Vector2<float>::make(0.0f, 0.0f),
        .scale = paradiso::Vector2<float>::make(0.0125f, 0.0125f),
    };

    // interaction Stage - Ball
    void interact(PongStage& stage) {

        auto touch = stage.touch(sprite.pivot);

        switch (touch) {
        case PongStage::TouchPoint::Top:
        case PongStage::TouchPoint::Bottom:
            velocity.y() *= -1;
            break;
        case PongStage::TouchPoint::Left:
        case PongStage::TouchPoint::Right:
            velocity.x() *= -1;
            break;
        default:
            break;
        }
    }

    // interact ball & paddle
    void interact(const PongPaddle& paddle) {
        const auto& ps = paddle.sprite.scale;
        const auto& pp = paddle.sprite.pivot;

        auto left_x = pp.x() - ps.x();
        auto right_x = pp.x() + ps.x();

        static constexpr float eps{0.01f};

        if (sprite.pivot.x() >= left_x && sprite.pivot.x() <= right_x &&
            std::abs(pp.y() - sprite.pivot.y()) < eps) {

            velocity.y() *= -1;
            velocity.x() += paddle.velocity_horizontal * paddle.whoopiness;
        }
    }

    void update() { sprite.pivot += velocity; }

    void draw(const paradiso::Shader& shader) {

        sprite.pivot.x() = std::clamp(sprite.pivot.x(), -0.5f, 0.5f);

        // update shader uniforms
        shader.set_uniform("pivot", sprite.pivot);
        shader.set_uniform("scale", sprite.scale);
        shader.set_uniform("rotation", sprite.rotation);

        renderer.draw(sprite, shader);
    }

    paradiso::Vector2<float> velocity{};

    paradiso::Renderer renderer{};

    constexpr void push(const auto& impulse) noexcept { velocity += impulse; }

    constexpr void whoop(const auto& whoopiness) noexcept {
        velocity *= whoopiness;
    }
};

auto main() -> int {

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

    // wir bauen ein Fenster ...
    window
        .set_size(paradiso::Size{.width = 720, .height = 720}) // ... Grösse
        .set_position(paradiso::Point{.x = 100, .y = 100})     // ... Position
        .set_title("PardiSO.Pong")                             // ... 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);

    auto paddle = PongPaddle{};
    auto ball = PongBall{};
    auto stage = PongStage{};

    ball.push(paradiso::Vector2<float>::make(0.0001f, -0.0005f));

    // 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 = window.client_size()
                    .maximal_extent() // wir wollen das
                                      // Seitenverhältnis beibehalten
    });

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

    // 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 {
        auto me = window.client_size().maximal_extent();

        ctx.set_viewport(paradiso::Rectangle{
            .position = paradiso::Point{.x = 0, .y = 0},
            .size = me // wir wollen das
                       // Seitenverhältnis beibehalten
        });

        // hier wird das eigentliche löschen des vorherigen Inhalts ausgelöst
        ctx.clear();

        paddle.on_keyboard(w.keyboard_input());

        // still here
        bool want_close =
            (w.keyboard_input().size() && w.keyboard_input().top().key == 'Q');

        if (!w.keyboard_input().empty()) {
            if (w.keyboard_input().top().key == 'R') {
                ball.sprite.pivot.x() = 0.0f;
                ball.sprite.pivot.y() = 0.9f;
            }
            // speed adjust
            if (w.keyboard_input().top().key == 'N') {

                std::cout << "Speed Up!\n";

                ball.push(paradiso::Vector2<float>::make(0.f, 0.01f));

            } else if (w.keyboard_input().top().key == 'M') {

                ball.push(paradiso::Vector2<float>::make(0.f, -0.01f));

                std::cout << "Speed Lower!\n";
            }
        }

        ball.interact(stage);
        ball.interact(paddle);

        ball.update();

        stage.draw(shader);
        paddle.draw(shader);
        ball.draw(shader);

        // ... signalisiere ob wir weitermachen wollen ...
        return !want_close;
    })) {
    };

    return 0;
}
