package dev.photosync.ui;

import dev.photosync.mcapi.Translator;
import dev.photosync.mcapi.render.RenderBridge;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Accessors;

/**
 * PhotoSync's visual vocabulary: the handful of compound shapes that appear on
 * more than one screen, drawn on top of {@code RenderBridge}'s primitives.
 *
 * <p>This is not a bag of helpers. It is the object that knows what a PhotoSync
 * panel looks like, and it owns the {@link Theme} because every one of these
 * decisions is a theme decision. Widgets and screens each hold one; nothing in
 * the module reaches for a colour without going through it.
 */
@Getter
@Accessors(fluent = true)
@RequiredArgsConstructor
public final class Chrome {

    private final Theme theme;
    private final Translator text;

    /** Shorthand for the common case of a key with no arguments. */
    public String translate(String key, Object... arguments) {
        return text.get(key, arguments);
    }

    // -----------------------------------------------------------------------
    // Surfaces
    // -----------------------------------------------------------------------

    /** Dims the whole screen so the panel reads as being in front of the world. */
    public void scrim(RenderBridge render) {
        render.fill(0, 0, render.width(), render.height(), theme.scrim());
    }

    /** The main window: filled body, one-pixel border, header strip along the top. */
    public void panel(RenderBridge render, Rect bounds) {
        render.fill(bounds.x(), bounds.y(), bounds.width(), bounds.height(), theme.panel());
        render.border(bounds.x(), bounds.y(), bounds.width(), bounds.height(), theme.panelBorder());
    }

    /** A recessed area -- list viewports and the timeline grid sit in one of these. */
    public void well(RenderBridge render, Rect bounds) {
        render.fill(bounds.x(), bounds.y(), bounds.width(), bounds.height(), theme.surfaceSunken());
    }

    public void divider(RenderBridge render, int x, int y, int width) {
        render.fill(x, y, width, 1, theme.panelBorder());
    }

    // -----------------------------------------------------------------------
    // Text
    // -----------------------------------------------------------------------

    public void label(RenderBridge render, String value, int x, int y, int argb) {
        render.text(value, x, y, argb, true);
    }

    /** Draws {@code value} centred horizontally within {@code bounds}. */
    public void centered(RenderBridge render, String value, Rect bounds, int argb) {
        int x = bounds.x() + (bounds.width() - render.textWidth(value)) / 2;
        int y = bounds.y() + (bounds.height() - render.lineHeight()) / 2 + 1;
        render.text(value, x, y, argb, true);
    }

    /** Draws {@code value}, vertically centred in {@code bounds} and clipped to its width. */
    public void fitted(RenderBridge render, String value, Rect bounds, int argb) {
        int y = bounds.y() + (bounds.height() - render.lineHeight()) / 2 + 1;
        render.text(elide(render, value, bounds.width()), bounds.x(), y, argb, true);
    }

    /**
     * Shortens {@code value} with an ellipsis until it fits.
     *
     * <p>Needed constantly: file names, album names, error messages and server
     * URLs are all attacker-of-layout length, and the alternative to eliding is
     * text spilling across a neighbouring column.
     */
    public String elide(RenderBridge render, String value, int maxWidth) {
        if (render.textWidth(value) <= maxWidth) {
            return value;
        }
        String ellipsis = "...";
        int budget = maxWidth - render.textWidth(ellipsis);
        if (budget <= 0) {
            return "";
        }
        // Linear from the end rather than a binary search: these strings are
        // short, and this runs a few dozen times a frame at most.
        int end = value.length();
        while (end > 0 && render.textWidth(value.substring(0, end)) > budget) {
            end--;
        }
        return value.substring(0, end) + ellipsis;
    }

    /** Wraps to at most {@code maxLines}, eliding the last one if it still overflows. */
    public java.util.List<String> wrap(RenderBridge render, String value, int maxWidth, int maxLines) {
        java.util.List<String> lines = new java.util.ArrayList<>();
        StringBuilder line = new StringBuilder();
        for (String word : value.split("\\s+")) {
            String candidate = line.isEmpty() ? word : line + " " + word;
            if (render.textWidth(candidate) <= maxWidth || line.isEmpty()) {
                line.setLength(0);
                line.append(candidate);
            } else {
                lines.add(line.toString());
                line.setLength(0);
                line.append(word);
                if (lines.size() == maxLines - 1) {
                    break;
                }
            }
        }
        if (!line.isEmpty() && lines.size() < maxLines) {
            lines.add(lines.size() == maxLines - 1 ? elide(render, line.toString(), maxWidth) : line.toString());
        }
        return lines;
    }

    // -----------------------------------------------------------------------
    // Indicators
    // -----------------------------------------------------------------------

    /** A filled bar. {@code fraction} is clamped, so a bad total cannot draw outside. */
    public void progressBar(RenderBridge render, Rect bounds, double fraction, int argb) {
        render.fill(bounds.x(), bounds.y(), bounds.width(), bounds.height(), theme.surfaceSunken());
        int filled = (int) Math.round(bounds.width() * Math.max(0.0, Math.min(1.0, fraction)));
        if (filled > 0) {
            render.fill(bounds.x(), bounds.y(), filled, bounds.height(), argb);
        }
    }

    /**
     * An indeterminate bar, for work whose size is not known yet.
     *
     * <p>Driven by wall-clock time rather than a tick counter so it keeps moving
     * while the game is paused -- which is exactly when the player is looking at
     * a PhotoSync screen.
     */
    public void busyBar(RenderBridge render, Rect bounds, long nowMillis, int argb) {
        render.fill(bounds.x(), bounds.y(), bounds.width(), bounds.height(), theme.surfaceSunken());
        int span = Math.max(8, bounds.width() / 4);
        int travel = bounds.width() + span;
        int offset = (int) ((nowMillis / 4) % travel) - span;
        int start = Math.max(bounds.x(), bounds.x() + offset);
        int end = Math.min(bounds.right(), bounds.x() + offset + span);
        if (end > start) {
            render.fill(start, bounds.y(), end - start, bounds.height(), argb);
        }
    }

    /** A small filled pill with a label, used for states and counts. */
    public void badge(RenderBridge render, String value, int x, int y, int background, int foreground) {
        int width = render.textWidth(value) + 6;
        int height = render.lineHeight() + 2;
        render.fill(x, y, width, height, background);
        render.text(value, x + 3, y + 2, foreground, false);
    }

    /**
     * The marker drawn over a video's still preview.
     *
     * <p>PhotoSync never plays video, so this is the whole of its video support:
     * a triangle in the corner that says "this is a clip, go and watch it
     * somewhere that can".
     */
    public void videoMarker(RenderBridge render, Rect tile, String duration) {
        int size = 9;
        int x = tile.right() - size - 3;
        int y = tile.bottom() - size - 3;
        render.fill(x - 1, y - 1, size + 2, size + 2, theme.overlay());
        // A play triangle from horizontal runs -- there is no primitive for a
        // polygon, and at nine pixels nobody can tell the difference.
        for (int row = 0; row < size; row++) {
            int distance = Math.abs(row - size / 2);
            int length = Math.max(1, (size / 2) - distance + 1);
            render.fill(x + 2, y + row, length, 1, theme.text());
        }
        if (duration != null && !duration.isEmpty()) {
            int width = render.textWidth(duration);
            render.fill(tile.x() + 2, tile.bottom() - render.lineHeight() - 3, width + 4, render.lineHeight() + 2,
                    theme.overlay());
            render.text(duration, tile.x() + 4, tile.bottom() - render.lineHeight() - 2, theme.text(), false);
        }
    }

    /** Centred message for an empty list, a load failure, or "not configured yet". */
    public void notice(RenderBridge render, Rect bounds, String headline, String detail) {
        int lineHeight = render.lineHeight();
        int totalHeight = detail == null || detail.isEmpty() ? lineHeight : lineHeight * 2 + 3;
        int top = bounds.y() + (bounds.height() - totalHeight) / 2;
        centered(render, headline, new Rect(bounds.x(), top, bounds.width(), lineHeight), theme.textMuted());
        if (detail != null && !detail.isEmpty()) {
            centered(render, elide(render, detail, bounds.width() - 16),
                    new Rect(bounds.x(), top + lineHeight + 3, bounds.width(), lineHeight), theme.textFaint());
        }
    }
}
