package dev.photosync.ui.widget;

import dev.photosync.mcapi.Keys;
import dev.photosync.mcapi.render.RenderBridge;
import dev.photosync.ui.Chrome;
import lombok.experimental.Accessors;

import java.util.function.IntConsumer;
import java.util.function.IntSupplier;

/**
 * An integer chosen by dragging, shown as the label the player actually cares
 * about rather than the raw number.
 *
 * <p>Every numeric setting in PhotoSync has a sensible range that the config
 * clamps to anyway, which is what makes a slider the right control here: it
 * cannot produce a value the mod would have to reject, so there is no error
 * state to design.
 */
@Accessors(fluent = true)
public final class Slider extends Widget {

    private final IntSupplier reader;
    private final IntConsumer writer;
    private final int minimum;
    private final int maximum;
    private final Formatter labels;

    /** Turns the raw value into the text drawn on the track, e.g. {@code 300 -> "5 min"}. */
    @FunctionalInterface
    public interface Formatter {
        String format(int value);
    }

    private boolean dragging;

    public Slider(Chrome chrome, IntSupplier reader, IntConsumer writer, int minimum, int maximum, Formatter labels) {
        super(chrome);
        this.reader = reader;
        this.writer = writer;
        this.minimum = minimum;
        this.maximum = Math.max(minimum + 1, maximum);
        this.labels = labels;
    }

    /** Focusable so the arrow keys can nudge it after a click. */
    @Override
    public boolean focusable() {
        return true;
    }

    @Override
    public void render(RenderBridge render, int mouseX, int mouseY) {
        boolean hovered = hovering(mouseX, mouseY);
        int value = clamp(reader.getAsInt());

        render.fill(bounds().x(), bounds().y(), bounds().width(), bounds().height(), theme().surfaceSunken());
        render.border(bounds().x(), bounds().y(), bounds().width(), bounds().height(),
                hovered || dragging ? theme().scrollThumbHover() : theme().panelBorder());

        int handleWidth = 6;
        int travel = bounds().width() - 2 - handleWidth;
        int handleX = bounds().x() + 1 + (int) Math.round(travel * fraction(value));
        render.fill(bounds().x() + 1, bounds().y() + 1, handleX - bounds().x() - 1, bounds().height() - 2,
                enabled() ? theme().fade(theme().accent(), 0.45f) : theme().surface());
        render.fill(handleX, bounds().y() + 1, handleWidth, bounds().height() - 2,
                enabled() ? (dragging || hovered ? theme().accentHover() : theme().accent()) : theme().textFaint());

        chrome.centered(render, labels.format(value), bounds(),
                enabled() ? theme().text() : theme().textFaint());
    }

    @Override
    public boolean mouseClicked(double mouseX, double mouseY, int button) {
        if (button != 0 || !hovering(mouseX, mouseY)) {
            return false;
        }
        dragging = true;
        applyAt(mouseX);
        return true;
    }

    @Override
    public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) {
        if (!dragging) {
            return false;
        }
        applyAt(mouseX);
        return true;
    }

    @Override
    public boolean mouseReleased(double mouseX, double mouseY, int button) {
        boolean was = dragging;
        dragging = false;
        return was;
    }

    @Override
    public boolean mouseScrolled(double mouseX, double mouseY, double amount) {
        if (!hovering(mouseX, mouseY)) {
            return false;
        }
        writer.accept(clamp(reader.getAsInt() + (int) Math.signum(amount) * step()));
        return true;
    }

    @Override
    public boolean keyPressed(int key, int scanCode, int modifiers) {
        int direction = key == Keys.LEFT ? -1 : key == Keys.RIGHT ? 1 : 0;
        if (direction == 0) {
            return false;
        }
        writer.accept(clamp(reader.getAsInt() + direction * step()));
        return true;
    }

    private void applyAt(double mouseX) {
        double ratio = (mouseX - bounds().x() - 3) / Math.max(1, bounds().width() - 6);
        writer.accept(clamp(minimum + (int) Math.round(ratio * (maximum - minimum))));
    }

    /** One percent of the range, so a keypress or wheel notch moves a useful amount. */
    private int step() {
        return Math.max(1, (maximum - minimum) / 100);
    }

    private double fraction(int value) {
        return (double) (value - minimum) / (maximum - minimum);
    }

    private int clamp(int value) {
        return Math.max(minimum, Math.min(maximum, value));
    }
}
