// Copyright 2014-2025 Jesper Larsson
//
// This file is part of Klipspringer, <https://klipspringer.avadeaux.net/>
//
// Klipspringer is free software: you can redistribute it and/or modify it under the terms of the
// GNU General Public License as published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// Klipspringer is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
// even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
// General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with Klipspringer. If
// not, see <https://www.gnu.org/licenses/>.

package net.avadeaux.klipspringer;

import java.io.*;
import net.avadeaux.klipspringer.codec.Device;

/**
 * Keypress interface of the track player. Should not run simultaneously with {@link
 * InteractiveCommandInterface}.
 */
public class TrackKeyInterface {
    public interface Control {
        void seek(int trackIx, double time, Device.Player.Factory outFact, Object... extra) throws IOException;
        default void pause(Device.Player.Factory resumeFact) throws IOException { }
        default void unpause() throws IOException { }
    }

    private final Control control;
    private final Device.Player.Factory serverOutFact;
    private final PlayMonitor state;
    private final int maxTrackIx;
    private boolean quit = false;
    private double restartTime;

    /** Creates a command interface with text input and output, acting on the given control and
      * querying the given state service for current state.
      */
    public TrackKeyInterface(Control control, Device.Player.Factory serverOutFact, PlayMonitor state, int maxTrackIx, double restartTime) {
        this.control = control;
        this.serverOutFact = serverOutFact;
        this.state = state;
        this.maxTrackIx = maxTrackIx;
        this.restartTime = restartTime;
    }

    public TrackKeyInterface(Control control, Device.Player.Factory serverOutFact, PlayMonitor state, int maxTrackIx) {
        this(control, serverOutFact, state, maxTrackIx, Double.NaN);
    }

    private void skip(int step) throws IOException {
        int trackIx = state.playingTrackIndex();
        if (trackIx < 0) { return; }
        trackIx += step;
        if (trackIx > maxTrackIx) { return; }
        control.seek(trackIx, 0, serverOutFact);
    }

    private void toggle(Query.Onoff extra) throws IOException {
        if (extra == Query.Onoff.DETECT) { return; }
        double time = state.playingTime();
        if (Double.isNaN(time)) { return; }
        control.seek(0, time, serverOutFact, Query.toggle(extra));
    }

    private void seekr(double off) throws IOException {
        double time = state.playingTime();
        time += off;
        if (Double.isNaN(time)) { return; }
        control.seek(0, time, serverOutFact, Query.Onoff.DETECT);
    }

    /** Call {@link Terminal#enableInterruptGetChar()} if this method is to be used. */
    public synchronized void quit() {
        if (quit) { return; }
        try {
            Terminal.interruptGetChar();
        } catch (Exception ex) {
            if (System.getProperty("klipspringer.debug") != null) {
                ex.printStackTrace();
            }
        }
        quit = true;
    }

    public void runUntilQuit() throws IOException {
        Terminal.startKeyListen();
        try {
            long lasttime = 0;
            int lastch = 0;
            while (true) {
                synchronized (this) {
                    if (quit) { break; }
                }
                int ch;
                ch = Terminal.getChar();
                long now = System.currentTimeMillis();
                switch (ch) {
                case ' ':
                    if (state.paused()) { control.unpause(); }
                    else                { control.pause(serverOutFact); }
                    break;
                case 'f':
                    toggle(state.fading());
                    break;
                case 'm':
                    toggle(state.monomixing());
                    break;
                case 'p':
                    restartTime = state.playingTime();
                    break;
                case 'q':
                    quit = true;
                    break;
                case 'r':
                    if (!Double.isNaN(restartTime)) { control.seek(0, restartTime, serverOutFact); }
                    break;
                case '<':
                    skip(lastch == '<' && now - lasttime < 1000 ? -1 : 0);
                    break;
                case '>':
                    skip(1);
                    break;
                case 0x1b:                      // ESC
                    if (Terminal.getChar() != '[') { break; }
                case 0x9b:                      // C1 CSI
                    ch = Terminal.getChar();
                    switch (ch) {
                    case 'A': seekr(+30); break;
                    case 'B': seekr(-30); break;
                    case 'C': seekr(+5); break;
                    case 'D': seekr(-5); break;
                    default:                    // gobble to end of control sequence
                        while (ch < 0x40 || ch > 0x7E) { ch = Terminal.getChar(); }
                    }
                    break;
                default:
                    // ignore
                }
                lasttime = now;
                lastch = ch;
            }
        } finally {
            try {
                Terminal.stopKeyListen();
            } catch (IOException ex) {
                if (System.getProperty("klipspringer.debug") != null) {
                    System.err.println("Error resetting terminal: "+ex);
                    ex.printStackTrace();
                }
            }
        }
    }
}
