package dev.photosync.ui.screen;

import dev.photosync.mcapi.render.RenderBridge;
import dev.photosync.mcapi.screen.ScreenModel;
import dev.photosync.ui.Chrome;
import dev.photosync.ui.PhotoSyncUi;
import dev.photosync.ui.Rect;
import dev.photosync.ui.Theme;
import dev.photosync.ui.widget.WidgetList;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import lombok.experimental.Accessors;

import java.util.ArrayList;
import java.util.List;

/**
 * The frame every PhotoSync screen sits in: a centred panel with a title, the
 * three tabs, a body and a status line.
 *
 * <p>Having one frame is what makes the mod feel like one thing. It also means
 * the tabs are always in the same place, so "where do I change the album?" has
 * the same answer whichever screen the player happens to be looking at -- which
 * is the whole of the interaction design brief: set it up once, then only ever
 * think about screenshots.
 *
 * <p>Subclasses fill in the body. They get a {@link WidgetList} that already
 * handles focus and mouse capture, and a laid-out rectangle to put things in.
 */
public abstract class PhotoSyncScreen implements ScreenModel {

    /** The three things the mod does, in the order a new player meets them. */
    @Getter
    @Accessors(fluent = true)
    @RequiredArgsConstructor
    public enum Tab {
        QUEUE("photosync.tab.queue"),
        BROWSE("photosync.tab.browse"),
        SETTINGS("photosync.tab.settings");

        private final String titleKey;
    }

    private static final int TAB_HEIGHT = 16;
    private static final int CLOSE_SIZE = 11;

    protected final PhotoSyncUi ui;
    protected final Chrome chrome;
    protected final WidgetList widgets = new WidgetList();

    private Rect panel = Rect.EMPTY;
    private Rect header = Rect.EMPTY;
    private Rect body = Rect.EMPTY;
    private Rect footer = Rect.EMPTY;
    private Rect closeButton = Rect.EMPTY;
    private final List<Rect> tabBounds = new ArrayList<>(Tab.values().length);

    protected PhotoSyncScreen(PhotoSyncUi ui) {
        this.ui = ui;
        this.chrome = ui.chrome();
    }

    // -----------------------------------------------------------------------
    // What subclasses provide
    // -----------------------------------------------------------------------

    protected abstract Tab tab();

    /** Lay out the body. The widget list has already been cleared. */
    protected abstract void layoutBody(Rect area);

    protected abstract void renderBody(RenderBridge render, int mouseX, int mouseY);

    /** The status line along the bottom. Empty for none. */
    protected String footerText() {
        return "";
    }

    protected int footerColour() {
        return theme().textFaint();
    }

    // -----------------------------------------------------------------------
    // Frame
    // -----------------------------------------------------------------------

    @Override
    public String title() {
        return chrome.translate(tab().titleKey());
    }

    protected Theme theme() {
        return chrome.theme();
    }

    protected Rect body() {
        return body;
    }

    @Override
    public final void layout(int width, int height) {
        Theme theme = theme();
        // Big enough for five columns of thumbnails, capped so it stays a panel
        // rather than swallowing the screen on a large display.
        int panelWidth = Math.max(220, Math.min(width - 24, 640));
        int panelHeight = Math.max(160, Math.min(height - 24, 420));
        panel = new Rect((width - panelWidth) / 2, (height - panelHeight) / 2, panelWidth, panelHeight);

        Rect inner = panel.inset(theme.padding());
        header = inner.top(theme.headerHeight());
        closeButton = new Rect(header.right() - CLOSE_SIZE, header.y() + 2, CLOSE_SIZE, CLOSE_SIZE);

        Rect tabRow = inner.dropTop(theme.headerHeight()).top(TAB_HEIGHT);
        tabBounds.clear();
        Tab[] all = Tab.values();
        int tabWidth = tabRow.width() / all.length;
        for (int i = 0; i < all.length; i++) {
            int x = tabRow.x() + i * tabWidth;
            int wide = i == all.length - 1 ? tabRow.right() - x : tabWidth;
            tabBounds.add(new Rect(x, tabRow.y(), wide, tabRow.height()));
        }

        Rect below = inner.dropTop(theme.headerHeight() + TAB_HEIGHT + theme.gap());
        footer = below.bottom(theme.footerHeight());
        body = below.dropBottom(theme.footerHeight() + theme.gap());

        widgets.clear();
        layoutBody(body);
    }

    @Override
    public final void render(RenderBridge render, int mouseX, int mouseY) {
        chrome.scrim(render);
        chrome.panel(render, panel);

        chrome.fitted(render, title(), header.dropRight(CLOSE_SIZE + 4).withHeight(theme().headerHeight() - 6),
                theme().text());
        renderClose(render, mouseX, mouseY);
        renderTabs(render, mouseX, mouseY);

        renderBody(render, mouseX, mouseY);
        widgets.render(render, mouseX, mouseY);

        String status = footerText();
        if (!status.isEmpty()) {
            chrome.fitted(render, chrome.elide(render, status, footer.width()), footer, footerColour());
        }
    }

    private void renderClose(RenderBridge render, int mouseX, int mouseY) {
        boolean hovered = closeButton.contains(mouseX, mouseY);
        int colour = hovered ? theme().danger() : theme().textMuted();
        // A cross from two diagonals; there is no line primitive and at eleven
        // pixels a glyph would sit off-centre in the box.
        for (int i = 2; i < CLOSE_SIZE - 2; i++) {
            render.fill(closeButton.x() + i, closeButton.y() + i, 1, 1, colour);
            render.fill(closeButton.x() + i, closeButton.bottom() - i - 1, 1, 1, colour);
        }
    }

    private void renderTabs(RenderBridge render, int mouseX, int mouseY) {
        Tab[] all = Tab.values();
        for (int i = 0; i < all.length; i++) {
            Rect bounds = tabBounds.get(i);
            boolean selected = all[i] == tab();
            boolean hovered = bounds.contains(mouseX, mouseY);
            render.fill(bounds.x(), bounds.y(), bounds.width(), bounds.height(),
                    selected ? theme().surface() : theme().fade(theme().surface(), hovered ? 0.6f : 0.25f));
            if (selected) {
                render.fill(bounds.x(), bounds.bottom() - 1, bounds.width(), 1, theme().accent());
            }
            chrome.centered(render, chrome.translate(all[i].titleKey()), bounds,
                    selected ? theme().text() : theme().textMuted());
        }
    }

    // -----------------------------------------------------------------------
    // Input
    // -----------------------------------------------------------------------

    @Override
    public void tick() {
        widgets.tick();
    }

    @Override
    public boolean mouseClicked(double mouseX, double mouseY, int button) {
        if (button == 0 && closeButton.contains(mouseX, mouseY)) {
            ui.close();
            return true;
        }
        if (button == 0) {
            for (int i = 0; i < tabBounds.size(); i++) {
                if (tabBounds.get(i).contains(mouseX, mouseY)) {
                    ui.open(Tab.values()[i]);
                    return true;
                }
            }
        }
        return widgets.mouseClicked(mouseX, mouseY, button);
    }

    @Override
    public boolean mouseReleased(double mouseX, double mouseY, int button) {
        return widgets.mouseReleased(mouseX, mouseY, button);
    }

    @Override
    public boolean mouseDragged(double mouseX, double mouseY, int button, double deltaX, double deltaY) {
        return widgets.mouseDragged(mouseX, mouseY, button, deltaX, deltaY);
    }

    @Override
    public boolean mouseScrolled(double mouseX, double mouseY, double amount) {
        return widgets.mouseScrolled(mouseX, mouseY, amount);
    }

    @Override
    public boolean keyPressed(int key, int scanCode, int modifiers) {
        return widgets.keyPressed(key, scanCode, modifiers);
    }

    @Override
    public boolean charTyped(char character, int modifiers) {
        return widgets.charTyped(character, modifiers);
    }
}
