/*
Crafter®.Graphics
Copyright (C) 2026 Catcrafts®
Catcrafts.net

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 3.0 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301 USA
*/
module;
module Crafter.Graphics;
import std;
import Crafter.Math;
import Crafter.Event;

using namespace Crafter;
using namespace Crafter::Input;

// ────────────────────────────────────────────────────────────────────
// Helpers
// ────────────────────────────────────────────────────────────────────
namespace {
    // Look up a connected gamepad by stable id. Returns nullptr if not
    // currently connected — the binding becomes inert until reconnect.
    Gamepad::Device* FindGamepad(std::uint32_t id) {
        return Gamepad::FindById(id);
    }

    float ApplyDeadzone(float v, float dz) {
        float a = std::abs(v);
        if (a <= dz) return 0.0f;
        float sign = v < 0 ? -1.0f : 1.0f;
        return sign * (a - dz) / (1.0f - dz);
    }

    Vector<float, 2> ApplyRadialDeadzone(Vector<float, 2> v, float dz) {
        float mag = std::sqrt(v.x * v.x + v.y * v.y);
        if (mag <= dz) return Vector<float, 2>{0.0f, 0.0f};
        float scaled = (mag - dz) / (1.0f - dz);
        float k = scaled / mag;
        return Vector<float, 2>{ v.x * k, v.y * k };
    }

    // ─── Evaluate one binding to its raw contribution. ───────────────
    // Buttons: bool. Axis: float. Vector2: pair.
    struct Eval {
        bool             button = false;
        float            value  = 0.0f;
        Vector<float, 2> vec    { 0.0f, 0.0f };
    };

    Eval EvalBinding(const Binding& b, Window* w, std::int32_t scrollAccum,
                     Vector<float, 2> mouseDelta, float deadzone)
    {
        Eval r;
        std::visit([&](auto const& bb) {
            using T = std::decay_t<decltype(bb)>;
            if constexpr (std::is_same_v<T, KeyBind>) {
                if (w && w->heldKeys.contains(bb.code)) r.button = true;
                r.value = r.button ? 1.0f : 0.0f;
            } else if constexpr (std::is_same_v<T, MouseButtonBind>) {
                if (w) {
                    if (bb.button == 0 && w->mouseLeftHeld)  r.button = true;
                    if (bb.button == 1 && w->mouseRightHeld) r.button = true;
                }
                r.value = r.button ? 1.0f : 0.0f;
            } else if constexpr (std::is_same_v<T, MouseScrollBind>) {
                r.value = (float)scrollAccum;
                r.button = scrollAccum != 0;
            } else if constexpr (std::is_same_v<T, MouseDeltaBind>) {
                r.vec = Vector<float, 2>{
                    mouseDelta.x * bb.scale,
                    mouseDelta.y * bb.scale
                };
            } else if constexpr (std::is_same_v<T, GamepadButtonBind>) {
                auto* gp = FindGamepad(bb.gamepadId);
                if (gp) r.button = gp->buttons[(std::size_t)bb.button];
                r.value = r.button ? 1.0f : 0.0f;
            } else if constexpr (std::is_same_v<T, GamepadAxisBind>) {
                auto* gp = FindGamepad(bb.gamepadId);
                if (gp) {
                    float v = gp->axes[(std::size_t)bb.axis];
                    if (bb.invert) v = -v;
                    // Triggers (0..1) and sticks (-1..1) share this path;
                    // deadzone applies symmetrically.
                    r.value = ApplyDeadzone(v, deadzone);
                    r.button = std::abs(r.value) > 0.0f;
                }
            } else if constexpr (std::is_same_v<T, GamepadStickBind>) {
                auto* gp = FindGamepad(bb.gamepadId);
                if (gp) {
                    using A = Gamepad::Axis;
                    A xAxis = (bb.stick == Gamepad::Stick::Left) ? A::LeftStickX : A::RightStickX;
                    A yAxis = (bb.stick == Gamepad::Stick::Left) ? A::LeftStickY : A::RightStickY;
                    Vector<float, 2> raw{
                        gp->axes[(std::size_t)xAxis],
                        gp->axes[(std::size_t)yAxis]
                    };
                    r.vec = ApplyRadialDeadzone(raw, deadzone);
                }
            } else if constexpr (std::is_same_v<T, WASDBind>) {
                Vector<float, 2> v{ 0.0f, 0.0f };
                if (w) {
                    if (w->heldKeys.contains(bb.left))  v.x -= 1.0f;
                    if (w->heldKeys.contains(bb.right)) v.x += 1.0f;
                    if (w->heldKeys.contains(bb.up))    v.y += 1.0f;
                    if (w->heldKeys.contains(bb.down))  v.y -= 1.0f;
                }
                r.vec = v;
            }
        }, b);
        return r;
    }

    // Combine bindings for the action's type. For Button: OR. For Axis1D:
    // sum-then-clamp (so two analog inputs add but cap at ±1; two
    // digital inputs cap at 1 too). For Vector2: per-axis sum-and-clamp.
    void EvaluateAction(Action& a, Window* w, std::int32_t scrollAccum,
                        Vector<float, 2> mouseDelta)
    {
        bool             nextPressed = false;
        float            nextValue   = 0.0f;
        Vector<float, 2> nextVec     { 0.0f, 0.0f };

        for (const Binding& b : a.bindings) {
            Eval e = EvalBinding(b, w, scrollAccum, mouseDelta, a.deadzone);
            if (e.button) nextPressed = true;
            nextValue   += e.value;
            nextVec.x   += e.vec.x;
            nextVec.y   += e.vec.y;
        }
        // Clamp scalars/vectors to ±1; sum semantics for combining.
        auto clamp = [](float v) { return v < -1.0f ? -1.0f : v > 1.0f ? 1.0f : v; };
        nextValue = clamp(nextValue);
        nextVec.x = clamp(nextVec.x);
        nextVec.y = clamp(nextVec.y);

        // Dispatch edges + value changes.
        if (a.type == ActionType::Button) {
            if (nextPressed && !a.pressed) {
                a.pressed = true;
                a.onPerformed.Invoke();
            } else if (!nextPressed && a.pressed) {
                a.pressed = false;
                a.onCanceled.Invoke();
            }
        } else if (a.type == ActionType::Axis1D) {
            bool wasNonZero = a.value != 0.0f;
            bool isNonZero  = nextValue != 0.0f;
            if (nextValue != a.value) {
                a.value = nextValue;
                a.onValueChanged.Invoke(nextValue);
            }
            if (isNonZero && !wasNonZero) a.onPerformed.Invoke();
            else if (!isNonZero && wasNonZero) a.onCanceled.Invoke();
        } else { // Vector2
            bool wasNonZero = (a.vector2.x != 0.0f || a.vector2.y != 0.0f);
            bool isNonZero  = (nextVec.x   != 0.0f || nextVec.y   != 0.0f);
            if (nextVec.x != a.vector2.x || nextVec.y != a.vector2.y) {
                a.vector2 = nextVec;
                a.onVector2Changed.Invoke(nextVec);
            }
            if (isNonZero && !wasNonZero) a.onPerformed.Invoke();
            else if (!isNonZero && wasNonZero) a.onCanceled.Invoke();
        }
    }

    // ─── Rebind detection ───────────────────────────────────────────
    // Scan for a fresh down-edge against the snapshot taken at
    // StartRebind. Returns the captured binding if found, nullopt if
    // nothing fresh is held. The first match wins; preference goes
    // keyboard > mouse > gamepad to keep the behavior predictable.
    std::optional<Binding> DetectRebindEdge(Map::RebindState& s, Window* w) {
        // Keyboard
        if (w && HasFlag(s.mask, CaptureMask::Keyboard)) {
            for (KeyCode c : w->heldKeys) {
                if (!s.keysHeldAtStart.contains(c)) {
                    return Binding{ KeyBind{c} };
                }
            }
        }
        // Mouse buttons
        if (w && HasFlag(s.mask, CaptureMask::Mouse)) {
            if (w->mouseLeftHeld && !s.mouseLeftHeldAtStart) {
                return Binding{ MouseButtonBind{0} };
            }
            if (w->mouseRightHeld && !s.mouseRightHeldAtStart) {
                return Binding{ MouseButtonBind{1} };
            }
        }
        // Gamepad buttons + axes
        if (HasFlag(s.mask, CaptureMask::Gamepad)) {
            for (auto& up : Gamepad::connected) {
                Gamepad::Device* gp = up.get();
                auto it = s.gamepadButtonsAtStart.find(gp->id);
                bool hadSnapshot = it != s.gamepadButtonsAtStart.end();
                for (std::size_t i = 0; i < (std::size_t)Gamepad::Button::Max; ++i) {
                    bool now    = gp->buttons[i];
                    bool before = hadSnapshot ? it->second[i] : false;
                    if (now && !before) {
                        return Binding{ GamepadButtonBind{
                            gp->id, (Gamepad::Button)i
                        } };
                    }
                }
                // Axes: any value past 0.5 captures as a 1-D axis bind.
                constexpr float kRebindAxisThreshold = 0.5f;
                for (std::size_t i = 0; i < (std::size_t)Gamepad::Axis::Max; ++i) {
                    float v = gp->axes[i];
                    if (std::abs(v) >= kRebindAxisThreshold) {
                        return Binding{ GamepadAxisBind{
                            gp->id, (Gamepad::Axis)i, v < 0.0f
                        } };
                    }
                }
            }
        }
        return std::nullopt;
    }
}

// ────────────────────────────────────────────────────────────────────
// Map
// ────────────────────────────────────────────────────────────────────
Action& Map::AddAction(std::string name, ActionType type) {
    auto up = std::make_unique<Action>();
    up->name = std::move(name);
    up->type = type;
    Action& ref = *up;
    actions.push_back(std::move(up));
    return ref;
}

Action* Map::Find(std::string_view name) {
    for (auto& up : actions) {
        if (up->name == name) return up.get();
    }
    return nullptr;
}

void Map::Attach(Window& w) {
    Detach();
    window = &w;
    // Only event-driven source we need is mouse scroll — everything
    // else (held keys, mouse buttons, mouse position, gamepad state) is
    // polled directly. Accumulate scroll between ticks so a flick that
    // fires multiple wheel events in one frame all count.
    scrollListener = std::make_unique<EventListener<std::uint32_t>>(
        &w.onMouseScroll,
        [this](std::uint32_t delta) {
            // Every backend (DOM, Wayland, Win32) delivers scroll as
            // signed whole detents (+1 = wheel down) packed into a
            // uint32; we reinterpret to preserve sign.
            scrollAccumulator += (std::int32_t)delta;
        });
    lastMousePos.reset();
    scrollAccumulator = 0;
}

void Map::Detach() {
    if (scrollListener) {
        scrollListener->Clear();
        scrollListener.reset();
    }
    window = nullptr;
    scrollAccumulator = 0;
    lastMousePos.reset();
    rebind.reset();
}

void Map::Tick() {
    // Mouse delta: derived from window position vs. last frame.
    Vector<float, 2> mouseDelta{ 0.0f, 0.0f };
    if (window) {
        if (lastMousePos.has_value()) {
            mouseDelta.x = window->currentMousePos.x - lastMousePos->x;
            mouseDelta.y = window->currentMousePos.y - lastMousePos->y;
        }
        lastMousePos = window->currentMousePos;
    }

    // Rebind takes priority. If a fresh edge is detected, fire the
    // callback and exit rebind mode. Actions don't evaluate this tick
    // for the captured input — the user usually wants the bind to take
    // effect on the NEXT press of that input.
    if (rebind.has_value()) {
        if (auto captured = DetectRebindEdge(*rebind, window)) {
            auto cb = std::move(rebind->onCaptured);
            rebind.reset();
            // Drain scroll so the next Tick doesn't double-fire on the
            // same wheel input that may have triggered the rebind.
            scrollAccumulator = 0;
            cb(*captured);
            return;
        }
    }

    std::int32_t scrollThisTick = scrollAccumulator;
    scrollAccumulator = 0;

    for (auto& up : actions) {
        EvaluateAction(*up, window, scrollThisTick, mouseDelta);
    }
}

void Map::StartRebind(Action& action, CaptureMask mask,
                      std::function<void(Binding)> onCaptured)
{
    RebindState s;
    s.action = &action;
    s.mask = mask;
    s.onCaptured = std::move(onCaptured);
    if (window) {
        s.keysHeldAtStart = window->heldKeys;
        s.mouseLeftHeldAtStart  = window->mouseLeftHeld;
        s.mouseRightHeldAtStart = window->mouseRightHeld;
    }
    for (auto& up : Gamepad::connected) {
        std::array<bool, (std::size_t)Gamepad::Button::Max> snap{};
        for (std::size_t i = 0; i < snap.size(); ++i) {
            snap[i] = up->buttons[i];
        }
        s.gamepadButtonsAtStart[up->id] = snap;
    }
    rebind.emplace(std::move(s));
}

void Map::StopRebind() {
    rebind.reset();
}

bool Map::IsRebinding() const {
    return rebind.has_value();
}

// ────────────────────────────────────────────────────────────────────
// Serialization
// ────────────────────────────────────────────────────────────────────
std::string Crafter::Input::BindingToString(const Binding& b) {
    std::string out;
    std::visit([&](auto const& bb) {
        using T = std::decay_t<decltype(bb)>;
        if constexpr (std::is_same_v<T, KeyBind>) {
            out = std::format("key:{:x}", bb.code);
        } else if constexpr (std::is_same_v<T, MouseButtonBind>) {
            out = std::format("mb:{}", (int)bb.button);
        } else if constexpr (std::is_same_v<T, MouseScrollBind>) {
            out = "mscroll";
        } else if constexpr (std::is_same_v<T, MouseDeltaBind>) {
            out = std::format("mdelta:{}", bb.scale);
        } else if constexpr (std::is_same_v<T, GamepadButtonBind>) {
            out = std::format("gpb:{}:{}", bb.gamepadId, (int)bb.button);
        } else if constexpr (std::is_same_v<T, GamepadAxisBind>) {
            out = std::format("gpa:{}:{}:{}", bb.gamepadId, (int)bb.axis, bb.invert ? 1 : 0);
        } else if constexpr (std::is_same_v<T, GamepadStickBind>) {
            out = std::format("gps:{}:{}", bb.gamepadId, (int)bb.stick);
        } else if constexpr (std::is_same_v<T, WASDBind>) {
            out = std::format("wasd:{:x}:{:x}:{:x}:{:x}",
                              bb.up, bb.down, bb.left, bb.right);
        }
    }, b);
    return out;
}

namespace {
    // Split on ':' and parse. Tolerates leading whitespace; rejects
    // malformed input by returning nullopt.
    std::vector<std::string_view> Split(std::string_view s) {
        std::vector<std::string_view> parts;
        std::size_t start = 0;
        for (std::size_t i = 0; i < s.size(); ++i) {
            if (s[i] == ':') {
                parts.emplace_back(s.substr(start, i - start));
                start = i + 1;
            }
        }
        parts.emplace_back(s.substr(start));
        return parts;
    }

    bool ParseHex(std::string_view s, std::uint32_t& out) {
        auto [p, ec] = std::from_chars(s.data(), s.data() + s.size(), out, 16);
        return ec == std::errc{} && p == s.data() + s.size();
    }
    bool ParseDec(std::string_view s, std::uint32_t& out) {
        auto [p, ec] = std::from_chars(s.data(), s.data() + s.size(), out, 10);
        return ec == std::errc{} && p == s.data() + s.size();
    }
    bool ParseFloat(std::string_view s, float& out) {
        auto [p, ec] = std::from_chars(s.data(), s.data() + s.size(), out);
        return ec == std::errc{} && p == s.data() + s.size();
    }
}

std::optional<Binding> Crafter::Input::BindingFromString(std::string_view s) {
    auto parts = Split(s);
    if (parts.empty()) return std::nullopt;
    const std::string_view tag = parts[0];

    if (tag == "key" && parts.size() == 2) {
        std::uint32_t v;
        if (!ParseHex(parts[1], v)) return std::nullopt;
        return Binding{ KeyBind{ v } };
    }
    if (tag == "mb" && parts.size() == 2) {
        std::uint32_t v;
        if (!ParseDec(parts[1], v) || v > 255) return std::nullopt;
        return Binding{ MouseButtonBind{ (std::uint8_t)v } };
    }
    if (tag == "mscroll") return Binding{ MouseScrollBind{} };
    if (tag == "mdelta" && parts.size() == 2) {
        float scale;
        if (!ParseFloat(parts[1], scale)) return std::nullopt;
        return Binding{ MouseDeltaBind{ scale } };
    }
    if (tag == "gpb" && parts.size() == 3) {
        std::uint32_t id, btn;
        if (!ParseDec(parts[1], id) || !ParseDec(parts[2], btn)) return std::nullopt;
        if (btn >= (std::uint32_t)Gamepad::Button::Max) return std::nullopt;
        return Binding{ GamepadButtonBind{ id, (Gamepad::Button)btn } };
    }
    if (tag == "gpa" && parts.size() == 4) {
        std::uint32_t id, ax, inv;
        if (!ParseDec(parts[1], id) || !ParseDec(parts[2], ax) || !ParseDec(parts[3], inv)) {
            return std::nullopt;
        }
        if (ax >= (std::uint32_t)Gamepad::Axis::Max) return std::nullopt;
        return Binding{ GamepadAxisBind{ id, (Gamepad::Axis)ax, inv != 0 } };
    }
    if (tag == "gps" && parts.size() == 3) {
        std::uint32_t id, st;
        if (!ParseDec(parts[1], id) || !ParseDec(parts[2], st)) return std::nullopt;
        if (st > 1) return std::nullopt;
        return Binding{ GamepadStickBind{ id, (Gamepad::Stick)st } };
    }
    if (tag == "wasd" && parts.size() == 5) {
        std::uint32_t u, d, l, r;
        if (!ParseHex(parts[1], u) || !ParseHex(parts[2], d) ||
            !ParseHex(parts[3], l) || !ParseHex(parts[4], r)) {
            return std::nullopt;
        }
        return Binding{ WASDBind{ u, d, l, r } };
    }
    return std::nullopt;
}
