package dev.photosync.ui.widget;

import dev.photosync.mcapi.Keys;
import dev.photosync.mcapi.render.RenderBridge;

import java.util.ArrayList;
import java.util.List;

/**
 * The widgets on one screen, and the two pieces of state that only make sense
 * for the set as a whole: which one has keyboard focus, and which one is
 * currently capturing the mouse.
 *
 * <p>Capture matters more than it looks. Without it, dragging a slider and
 * letting the cursor slip off the track drops the drag, which every player
 * notices and nobody reports as a bug -- they just conclude the slider is
 * fiddly.
 */
public final class WidgetList {

    private final List<Widget> widgets = new ArrayList<>();

    private Widget focused;
    private Widget capturing;

    /** Returns its argument so a screen can add and keep a reference in one line. */
    public <W extends Widget> W add(W widget) {
        widgets.add(widget);
        return widget;
    }

    public void clear() {
        // Through focus(null) rather than by assignment, so the outgoing widget
        // is told it lost focus and stops drawing a caret.
        focus(null);
        capturing = null;
        widgets.clear();
    }

    public void render(RenderBridge render, int mouseX, int mouseY) {
        for (Widget widget : widgets) {
            if (widget.visible()) {
                widget.render(render, mouseX, mouseY);
            }
        }
    }

    public void tick() {
        widgets.forEach(Widget::tick);
    }

    public boolean mouseClicked(double mouseX, double mouseY, int button) {
        // Reverse order so the widget drawn last -- and therefore on top --
        // gets first refusal on the click.
        for (int i = widgets.size() - 1; i >= 0; i--) {
            Widget widget = widgets.get(i);
            if (!widget.active()) {
                continue;
            }
            if (widget.mouseClicked(mouseX, mouseY, button)) {
                capturing = widget;
                focus(widget.focusable() ? widget : null);
                return true;
            }
        }
        // A click on empty space commits whatever was being typed.
        focus(null);
        return false;
    }

    public boolean mouseReleased(double mouseX, double mouseY, int button) {
        Widget target = capturing;
        capturing = null;
        return target != null && target.mouseReleased(mouseX, mouseY, button);
    }

    public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) {
        return capturing != null && capturing.mouseDragged(mouseX, mouseY, button, deltaX, deltaY);
    }

    public boolean mouseScrolled(double mouseX, double mouseY, double amount) {
        for (int i = widgets.size() - 1; i >= 0; i--) {
            Widget widget = widgets.get(i);
            if (widget.active() && widget.mouseScrolled(mouseX, mouseY, amount)) {
                return true;
            }
        }
        return false;
    }

    public boolean keyPressed(int key, int scanCode, int modifiers) {
        if (focused != null && focused.active() && focused.keyPressed(key, scanCode, modifiers)) {
            return true;
        }
        if (key == Keys.TAB && !widgets.isEmpty()) {
            return cycleFocus(Keys.shift(modifiers) ? -1 : 1);
        }
        return false;
    }

    public boolean charTyped(char character, int modifiers) {
        return focused != null && focused.active() && focused.charTyped(character, modifiers);
    }

    /** True while a text field is taking input, so Escape can close the field rather than the screen. */
    public boolean hasFocus() {
        return focused != null;
    }

    public void focus(Widget widget) {
        if (focused == widget) {
            return;
        }
        if (focused != null) {
            focused.focus(false);
        }
        focused = widget;
        if (focused != null) {
            focused.focus(true);
        }
    }

    private boolean cycleFocus(int direction) {
        List<Widget> candidates = widgets.stream().filter(w -> w.active() && w.focusable()).toList();
        if (candidates.isEmpty()) {
            return false;
        }
        int current = candidates.indexOf(focused);
        // With nothing focused yet, Tab starts at the first widget and
        // shift-Tab at the last, rather than wherever -1 + direction lands.
        int next = current < 0
                ? (direction > 0 ? 0 : candidates.size() - 1)
                : Math.floorMod(current + direction, candidates.size());
        focus(candidates.get(next));
        return true;
    }
}
