package dev.photosync.ui.widget;

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

import java.util.function.BooleanSupplier;
import java.util.function.Consumer;

/**
 * An on/off switch with a label, and an optional line of explanation under it.
 *
 * <p>It reads and writes through a supplier and a consumer rather than holding
 * a boolean. The settings screen edits a draft config, and a widget with its own
 * copy of the value is a second source of truth that drifts the moment anything
 * else -- a reset button, a provider change -- touches the draft.
 */
@Accessors(fluent = true)
public final class Toggle extends Widget {

    private final String label;
    private final BooleanSupplier reader;
    private final Consumer<Boolean> writer;

    @Setter
    private String description;

    public Toggle(Chrome chrome, String label, BooleanSupplier reader, Consumer<Boolean> writer) {
        super(chrome);
        this.label = label;
        this.reader = reader;
        this.writer = writer;
    }

    public Toggle describedAs(String value) {
        this.description = value;
        return this;
    }

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

        int size = 11;
        int boxY = bounds().y() + (render.lineHeight() - size) / 2 + 1;
        Rect box = new Rect(bounds().x(), boxY, size, size);

        render.fill(box.x(), box.y(), box.width(), box.height(),
                on ? theme().accent() : theme().surfaceFor(hovered, false));
        render.border(box.x(), box.y(), box.width(), box.height(),
                on ? theme().accentHover() : hovered ? theme().scrollThumbHover() : theme().panelBorder());
        if (on) {
            // A check mark from two runs: a short down-right stroke and a long
            // up-right one. Legible at eleven pixels, which a glyph would not be.
            for (int i = 0; i < 3; i++) {
                render.fill(box.x() + 2 + i, box.y() + 4 + i, 1, 2, theme().accentText());
            }
            for (int i = 0; i < 4; i++) {
                render.fill(box.x() + 5 + i, box.y() + 6 - i, 1, 2, theme().accentText());
            }
        }

        int textX = box.right() + 6;
        int colour = enabled() ? theme().text() : theme().textFaint();
        render.text(chrome.elide(render, label, bounds().right() - textX), textX, bounds().y() + 1, colour, true);
        if (description != null && !description.isEmpty()) {
            render.text(chrome.elide(render, description, bounds().right() - textX),
                    textX, bounds().y() + render.lineHeight() + 2, theme().textFaint(), false);
        }
    }

    @Override
    public boolean mouseClicked(double mouseX, double mouseY, int button) {
        if (button != 0 || !hovering(mouseX, mouseY)) {
            return false;
        }
        writer.accept(!reader.getAsBoolean());
        return true;
    }

    /** Two lines when there is a description, one otherwise. */
    public int preferredHeight(RenderBridge render) {
        return description == null || description.isEmpty()
                ? render.lineHeight() + 4
                : render.lineHeight() * 2 + 4;
    }
}
