package dev.photosync.ui.notify;

import dev.photosync.core.config.NotificationKind;
import dev.photosync.core.config.NotificationSettings;
import dev.photosync.mcapi.render.RenderBridge;
import dev.photosync.ui.Chrome;
import dev.photosync.ui.Theme;

import java.util.List;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.function.Supplier;

/**
 * The one-line messages in the bottom-left corner.
 *
 * <p>Sized to be ignorable. A player who takes a screenshot already knows they
 * took one; the message is there to say the upload worked, and it has earned
 * about a second and a half of the corner of their eye for that. So: no icon, no
 * panel, no sound, no animation beyond a fade -- one line of text that leaves.
 *
 * <p>{@link #show} is called from upload workers and from the capture thread, so
 * the list is concurrent. Everything else happens on the render thread.
 */
public final class Notifications {

    /** Above this, the oldest is dropped rather than growing a wall of text. */
    private static final int MAX_VISIBLE = 3;
    private static final long FADE_MILLIS = 400;

    private record Toast(NotificationKind kind, String message, long expiresAt) {
    }

    private final Chrome chrome;
    private final Supplier<NotificationSettings> settings;
    private final List<Toast> live = new CopyOnWriteArrayList<>();

    public Notifications(Chrome chrome, Supplier<NotificationSettings> settings) {
        this.chrome = chrome;
        this.settings = settings;
    }

    /**
     * Posts a message, if the player has that kind switched on. Safe to call
     * from any thread.
     *
     * @param message already translated -- the caller knows the arguments
     */
    public void show(NotificationKind kind, String message) {
        NotificationSettings current = settings.get();
        if (!current.shows(kind)) {
            return;
        }
        live.add(new Toast(kind, message, System.currentTimeMillis() + current.lingerMillis()));
        while (live.size() > MAX_VISIBLE) {
            live.remove(0);
        }
    }

    /** Drops everything on screen -- used when the player turns notifications off. */
    public void clear() {
        live.clear();
    }

    /**
     * Draws the stack, newest at the bottom.
     *
     * <p>Anchored to the very bottom of the screen rather than above it: that
     * strip is to the left of the hotbar and below the chat, which is the only
     * part of the corner that is reliably empty.
     */
    public void render(RenderBridge render, long nowMillis) {
        live.removeIf(toast -> toast.expiresAt() <= nowMillis);
        if (live.isEmpty()) {
            return;
        }
        Theme theme = chrome.theme();
        int line = render.lineHeight() + 2;
        int y = render.height() - 4 - line;
        // Reverse order so the newest sits at the bottom and older ones rise.
        for (int i = live.size() - 1; i >= 0; i--) {
            Toast toast = live.get(i);
            float alpha = fade(toast, nowMillis);
            int background = theme.fade(theme.panel(), alpha * 0.85f);
            int width = render.textWidth(toast.message()) + 8;
            render.fill(4, y, width, line, background);
            render.fill(4, y, 1, line, theme.fade(colour(theme, toast.kind()), alpha));
            render.text(toast.message(), 8, y + 2, theme.fade(theme.text(), alpha), false);
            y -= line + 2;
        }
    }

    private float fade(Toast toast, long nowMillis) {
        long remaining = toast.expiresAt() - nowMillis;
        return remaining >= FADE_MILLIS ? 1f : Math.max(0f, remaining / (float) FADE_MILLIS);
    }

    private int colour(Theme theme, NotificationKind kind) {
        return switch (kind) {
            case CAPTURED -> theme.accent();
            case UPLOADED -> theme.success();
            case FAILED -> theme.danger();
        };
    }
}
