package dev.photosync.ui;

import dev.photosync.core.PhotoSync;
import dev.photosync.core.config.PhotoSyncConfig;
import dev.photosync.core.provider.Album;
import dev.photosync.core.provider.AlbumRef;
import dev.photosync.mcapi.ClientBridge;
import dev.photosync.mcapi.GameContext;
import dev.photosync.mcapi.lifecycle.QuitGuard;
import dev.photosync.ui.notify.Notifications;
import dev.photosync.ui.screen.AlbumPickerScreen;
import dev.photosync.ui.screen.PhotoSyncScreen;
import dev.photosync.ui.screen.PhotoSyncScreen.Tab;
import dev.photosync.ui.screen.QueueScreen;
import dev.photosync.ui.screen.QuitDialog;
import dev.photosync.ui.screen.SettingsScreen;
import dev.photosync.ui.screen.TimelineScreen;
import lombok.Getter;
import lombok.experimental.Accessors;

import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * What the screens share: the game, the core, and the handful of decisions that
 * outlive any one of them.
 *
 * <p>Screens are cheap and short-lived -- switching tabs builds a new one -- so
 * anything that must survive a tab switch lives here rather than in a screen.
 * That is three things: which album the browser is pointed at, the settings
 * draft, and the album names the picker has seen.
 *
 * <p>The draft deserves a word. It is null while the saved config is what the
 * player is looking at, and non-null once they have edited something. That makes
 * "is there anything to save?" a null check plus an equality test on two records,
 * rather than a dirty flag that someone has to remember to clear.
 */
@Accessors(fluent = true)
public final class PhotoSyncUi {

    @Getter
    private final PhotoSync core;
    @Getter
    private final ClientBridge bridge;
    @Getter
    private final Chrome chrome;
    @Getter
    private final Notifications notifications;

    /** Album display names learned from the picker, so settings can show one. */
    private final Map<String, String> albumNames = new ConcurrentHashMap<>();

    /**
     * Whether the browser is showing everything rather than the upload album.
     * A view preference, not a setting -- it is not worth persisting, and the
     * album the player configured is the right thing to open on.
     */
    @Getter
    private boolean browsingLibrary;

    private PhotoSyncConfig draft;
    private AlbumRef browsingTarget;

    public PhotoSyncUi(PhotoSync core, ClientBridge bridge, Theme theme) {
        this.core = core;
        this.bridge = bridge;
        this.chrome = new Chrome(theme, bridge.text());
        this.notifications = new Notifications(chrome, () -> core.config().current().notifications());
    }

    public GameContext game() {
        return bridge.game();
    }

    // -----------------------------------------------------------------------
    // Navigation
    // -----------------------------------------------------------------------

    /** The entry point the key binding calls. */
    public void open() {
        open(core.config().current().isReady() ? Tab.QUEUE : Tab.SETTINGS);
    }

    public void open(Tab tab) {
        bridge.screens().open(screenFor(tab));
    }

    public void openAlbumPicker() {
        bridge.screens().open(new AlbumPickerScreen(this));
    }

    public void close() {
        bridge.screens().close();
    }

    private PhotoSyncScreen screenFor(Tab tab) {
        return switch (tab) {
            case QUEUE -> new QueueScreen(this);
            case BROWSE -> new TimelineScreen(this);
            case SETTINGS -> new SettingsScreen(this);
        };
    }

    // -----------------------------------------------------------------------
    // Browsing
    // -----------------------------------------------------------------------

    public void browsingLibrary(boolean value) {
        this.browsingLibrary = value;
        ensureBrowsing();
    }

    /**
     * Points the browser at whatever the player is currently asking to see.
     *
     * <p>Called on every layout, so it has to be free when nothing changed:
     * {@code TimelineBrowser.open} throws away everything it has loaded, which
     * would turn a window resize into a full reload.
     */
    public void ensureBrowsing() {
        AlbumRef target = browsingLibrary ? AlbumRef.library() : core.config().current().album();
        if (target.equals(browsingTarget)) {
            return;
        }
        browsingTarget = target;
        core.browser().open(target);
    }

    // -----------------------------------------------------------------------
    // The settings draft
    // -----------------------------------------------------------------------

    /** The config the settings screen is editing: the draft, or the saved one. */
    public PhotoSyncConfig draft() {
        return draft == null ? core.config().current() : draft;
    }

    public void draft(PhotoSyncConfig edited) {
        this.draft = edited;
    }

    public boolean draftIsDirty() {
        return draft != null && !draft.equals(core.config().current());
    }

    /** Throws the edits away; the screen rebuilds from the saved config. */
    public void resetDraft() {
        this.draft = null;
    }

    /**
     * Saves the draft. Called by the Save button and again when the settings
     * screen closes, so an edit cannot be lost by pressing Escape -- which is
     * what a player who has finished typing will do.
     */
    public void applyDraft() {
        PhotoSyncConfig pending = draft;
        this.draft = null;
        if (pending == null || pending.equals(core.config().current())) {
            return;
        }
        core.config().update(current -> pending);
        // The album may have moved with it, and the browser is showing the old one.
        ensureBrowsing();
    }

    // -----------------------------------------------------------------------
    // Albums
    // -----------------------------------------------------------------------

    public void rememberAlbums(List<Album> albums) {
        albums.forEach(album -> albumNames.put(album.id(), album.name()));
    }

    /** What the settings screen's album button says. */
    public String albumLabel() {
        String id = draft().albumId();
        if (id.isEmpty()) {
            return chrome.translate("photosync.album.library");
        }
        // The id is a poor label, but it is honest: it means the player picked
        // this album on another machine and we have not seen the list yet.
        return albumNames.getOrDefault(id, id);
    }

    /** Chosen from the server's own list, so it is saved rather than drafted. */
    public void chooseAlbum(String albumId) {
        core.config().update(current -> current.toBuilder().albumId(albumId).build());
        if (draft != null) {
            draft = draft.toBuilder().albumId(albumId).build();
        }
        ensureBrowsing();
    }

    // -----------------------------------------------------------------------
    // Quitting
    // -----------------------------------------------------------------------

    /**
     * Puts the quit dialog up. Called from the {@link QuitGuard} handler.
     *
     * <p>Idempotent, and it has to be: closing the window sets a flag GLFW keeps
     * set, so the client asks the guard again on every frame until it either
     * gets a yes or the player clicks something. Re-opening the dialog each time
     * would reset its state and eat the click.
     */
    public void confirmQuit() {
        if (bridge.screens().current().filter(QuitDialog.class::isInstance).isPresent()) {
            return;
        }
        bridge.screens().open(new QuitDialog(this));
    }

    /** Opens the gate and lets the game go. */
    public void quitNow() {
        QuitGuard.get().allowOnce();
        bridge.screens().close();
        game().quit();
    }
}
