package dev.photosync.client;

import dev.photosync.core.capture.CameraPose;
import dev.photosync.core.capture.CaptureOrigin;
import dev.photosync.core.capture.CapturedScreenshot;
import dev.photosync.core.config.AutoCaptureSettings;
import dev.photosync.mcapi.GameContext;
import dev.photosync.mcapi.capture.ScreenshotService;
import lombok.extern.slf4j.Slf4j;

import java.io.IOException;
import java.nio.file.Path;
import java.util.Optional;
import java.util.function.Consumer;
import java.util.function.Supplier;

/**
 * The timer behind the optional "take a screenshot every N seconds" setting.
 *
 * <p>Measured against the wall clock rather than counted in ticks. Ticks stop
 * when the game is paused, which would make a five-minute interval mean
 * something different depending on how long the player spent in their inventory,
 * and the feature is only comprehensible if the number on the slider is the
 * number of seconds that elapse.
 *
 * <p>Off by default, and it stays off until the player says otherwise: silently
 * filling someone's screenshots folder is not a feature.
 *
 * <p>When it is on, a cycle is skipped if the camera has not moved for a whole
 * interval, because that shot would be the previous one again. The condition is
 * "has not moved since the last capture", expressed as a duration so that it
 * survives the player wandering off and coming back to the same spot.
 */
@Slf4j
public final class AutoCapture {

    private final GameContext game;
    private final ScreenshotService screenshots;
    private final Supplier<AutoCaptureSettings> settings;
    private final Consumer<CapturedScreenshot> sink;

    /** Zero means "not armed": the next tick sets the first deadline. */
    private long nextCaptureMillis;
    private boolean capturing;

    /** The last view that differed from the one before it, and when it arrived. */
    private CameraPose restingPose;
    private long movedAtMillis;

    public AutoCapture(GameContext game, ScreenshotService screenshots,
                       Supplier<AutoCaptureSettings> settings, Consumer<CapturedScreenshot> sink) {
        this.game = game;
        this.screenshots = screenshots;
        this.settings = settings;
        this.sink = sink;
    }

    /** Called once per client tick, from the render thread. */
    public void tick() {
        AutoCaptureSettings current = settings.get();
        if (!current.enabled()) {
            // Disarmed, so switching the feature on never fires immediately --
            // it always waits a full interval first.
            nextCaptureMillis = 0;
            restingPose = null;
            return;
        }

        long now = System.currentTimeMillis();
        long interval = current.intervalSeconds() * 1000L;
        // Every tick, so that stillness is measured over the whole interval
        // rather than sampled at its two ends.
        observePose(now);
        if (nextCaptureMillis == 0) {
            nextCaptureMillis = now + interval;
            return;
        }
        if (now < nextCaptureMillis || capturing) {
            return;
        }

        if (current.onlyInWorld() && !game.inWorld()) {
            // Sitting in the menus is a long-lived state; hold the deadline
            // forward so joining a world does not trigger an immediate burst.
            nextCaptureMillis = now + interval;
            return;
        }
        if (current.skipWhenScreenOpen() && game.screenOpen()) {
            // An open screen is momentary by comparison, so the deadline stays
            // where it is and the shot happens as soon as it closes.
            return;
        }
        if (current.skipWhenStill() && stillFor(interval, now)) {
            // The view is the one the last screenshot already has. Skip this
            // cycle and arm the next: the moment the player moves, the timer
            // resumes on its own, so nothing has to be re-enabled.
            log.debug("Skipping an automatic capture: the view has not changed in {}s",
                    current.intervalSeconds());
            nextCaptureMillis = now + interval;
            return;
        }

        nextCaptureMillis = now + interval;
        take(current.fileNameSuffix());
    }

    /**
     * Notes where the camera is, and when it last went somewhere new.
     *
     * <p>The comparison is against the last pose that <em>differed</em> rather
     * than against the previous tick, so a player drifting slowly -- in water, on
     * a boat, turning by a pixel at a time -- still counts as moving once the
     * movement adds up, instead of being called still forever.
     */
    private void observePose(long now) {
        Optional<CameraPose> pose = game.cameraPose();
        if (pose.isEmpty()) {
            // No world, or no player in it yet. Nothing to compare, and stillness
            // is not claimed against a view that does not exist.
            restingPose = null;
            return;
        }
        if (restingPose == null || pose.get().movedFrom(restingPose)) {
            restingPose = pose.get();
            movedAtMillis = now;
        }
    }

    /** True once the view has been the same for a full capture cycle. */
    private boolean stillFor(long millis, long now) {
        return restingPose != null && now - movedAtMillis >= millis;
    }

    private void take(String suffix) {
        capturing = true;
        screenshots.capture(suffix).whenComplete((file, failure) -> {
            capturing = false;
            if (failure != null) {
                log.warn("Automatic capture failed", failure);
                return;
            }
            publish(file);
        });
    }

    private void publish(Path file) {
        try {
            sink.accept(CapturedScreenshot.of(file, CaptureOrigin.AUTOMATIC));
        } catch (IOException e) {
            log.warn("Wrote {} but could not read it back", file, e);
        }
    }
}
