// 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 java.nio.file.Path;
import java.util.Arrays;
import org.json.JSONArray;
import net.avadeaux.klipspringer.codec.*;

/**
 * Player class with main method.
 */
public class TrackPlayer {
    private String output = null;
    private int xbufbytes = 262144;
    private double obufsec = 2.0;
    private int urunreco = 2;
    private Query.Onoff monomix = Query.Onoff.OFF;
    private String controlsock = null;
    private int controlthreads = 20;
    private int trackoff = 1, starttrack = 1;
    private double startseek = 0;
    private boolean cmdintf = true;
    private boolean quiet = false;
    private JSONArray mixSpecJson = null;
    private OutputSpec.Notify noti = new OutputSpec.Notify();

    void run(String[] args) {
        String helpText = "Arguments: [options] audiofiles\n\n"
            +"Options:\n"
            +"-output <string>        PCM output device or URL. Default is standard output device\n"
            +"-xbufbytes <int>        Minimum internal PCM buffer size. Default is "+xbufbytes+"\n"
            +"-obufsec <float>        External buffer time on PCM output. Default is "+obufsec+"\n"
            +"-urunreco <int>         Times to attempt recovery after buffer underrun. Default is "+urunreco+"\n"
            +"-controlsock <path>     Socket path for control server. By default, there is no control server\n"
            +"-controlthreads <int>   Max threads in control server. Default is "+controlthreads+"\n"
            +"-cmdintf <boolean>      Use interactive command interface when applicable. Default is "+cmdintf+"\n"
            +"-q                      Suppress TTY output\n"
            +"-mixjson <data>         Set mixing data for tracks. Default is no mixing.\n"
            +"-monomix <on|off>       Set all audio channels to be mixed equal. Default is "+monomix.toString().toLowerCase()+"\n"
            +"-trackoff <int>         Number of first track. Default is "+trackoff+"\n"
            +"-starttrack <int>       File to start playing from. Default is "+starttrack+"\n"
            +"-seek <float>           Start playing number of seconds into the track\n"
            +"-help or --help         Print this message\n"
            +"--                      Remaining arguments are file names\n\n"
            +"TTY key commands:\n"
            +"  SPACE                 Pause/play\n"
            +"  <                     Skip track backwards\n"
            +"  >                     Skip track ahead\n"
            +"  UP ARROW              Skip 30 seconds ahead\n"
            +"  DOWN ARROW            Skip 30 seconds back\n"
            +"  RIGHT ARROW           Skip 5 seconds ahead\n"
            +"  LEFT ARROW            Skip 5 seconds back\n"
            +"  m                     Monomix on/off\n"
            +"  p                     Save position\n"
            +"  r                     Restore position\n"
            +"  q                     Quit\n";
        if (args.length == 0) {
            System.err.println(helpText);
            System.exit(2);
        }

        int argPos = 0;
        while (argPos < args.length) {
            String o = args[argPos];
            if      (o.charAt(0) != '-')            { break; }
            argPos++;
            if      ("--"               .equals(o)) { break; }
            else if ("-q"               .equals(o)) { quiet = true; continue; }
            String v = argPos < args.length ? args[argPos++] : "";
            if      ("-output"          .equals(o)) { output           = v; }
            else if ("-xbufbytes"       .equals(o)) { xbufbytes        = Integer.parseInt(v); }
            else if ("-obufsec"         .equals(o)) { obufsec          = Double.parseDouble(v); }
            else if ("-urunreco"        .equals(o)) { urunreco         = Integer.parseInt(v); }
            else if ("-controlsock"     .equals(o)) { controlsock      = v; }
            else if ("-controlthreads"  .equals(o)) { controlthreads   = Integer.parseInt(v); }
            else if ("-cmdintf"         .equals(o)) { cmdintf          = Boolean.parseBoolean(v); }
            else if ("-mixjson"         .equals(o)) { mixSpecJson      = new JSONArray(v); }
            else if ("-monomix"         .equals(o)) { monomix          = Query.Onoff.valueOf(v.toUpperCase()); }
            else if ("-trackoff"        .equals(o)) { trackoff         = Integer.parseInt(v); }
            else if ("-starttrack"      .equals(o)) { starttrack       = Integer.parseInt(v); }
            else if ("-seek"            .equals(o)) { startseek        = Double.parseDouble(v); }
            else if (o.startsWith("-h") || o.startsWith("--h")) { System.out.println(helpText); return; }
            else if (o.length() == 0) {
                System.err.println("Empty files argument (use -help for usage)");
                System.exit(2);
            } else if (o.charAt(0) == '-') {
                System.err.println("Unregognized option "+o+" (use -help for usage)");
                System.exit(2);
            } else {
                break;
            }
        }
        String[] fnams = Arrays.copyOfRange(args, argPos, args.length);
        MixSpec[] mixs = new MixSpec[fnams.length];
        if (mixSpecJson == null) {
            Arrays.fill(mixs, MixSpec.getInstance());
        } else {
            for (int i = 0; i < fnams.length; i++) {
                mixs[i] = MixSpec.getInstance(mixSpecJson.getJSONObject(i));
            }
        }
        Track.List tracks = new Track.List(fnams, mixs, trackoff);
        PlayState state = new PlayState(tracks);
        TrackTerminalDisplay display = null;
        QueueExecutor streamKeepAliveExecutor = new QueueExecutor("Stream keep-alive");
        try {
            Device.Player.Factory outFact = OutputSpec.process(output, obufsec, urunreco, noti);
            TrackConsumer consumer = new TrackConsumer(tracks, state, xbufbytes, outFact == null ? 120000 : 800);
            TrackFeeder feeder = new TrackFeeder(tracks, state, consumer);
            CommandInterface cmdExec = new TrackCommandInterface(feeder, tracks, state, outFact, streamKeepAliveExecutor, 5000);
            if (cmdintf && outFact instanceof OutputSpec.LocalDeviceOutput && Terminal.inTty()) {
                TrackKeyInterface intf = new TrackKeyInterface(feeder, outFact, state, tracks.length-1);
                TrackTerminalDisplay d = display;
                Thread t = new Thread("Interactive Interface") {
                        public void run() {
                            try {
                                intf.runUntilQuit();
                            } catch (Throwable th) {
                                if (d != null && Terminal.errTty()) { d.quit(); }
                                System.err.println(th);
                                if (System.getProperty("klipspringer.debug") != null) { th.printStackTrace(); }
                                System.exit(1);
                            }
                            System.exit(0);
                        }
                    };
                t.setDaemon(true);
                t.start();
            }
            if (!quiet && outFact instanceof OutputSpec.LocalDeviceOutput && Terminal.outTty()) {
                display = new TrackTerminalDisplay(state);
                new Thread(display, "Terminal display").start();
            }
            if (outFact != null) { feeder.seek(starttrack-trackoff, startseek, outFact, monomix, null); }
            if (controlsock != null && controlsock.length() > 0) {
                Path sock = Path.of(controlsock);
                AsyncCommandServer cmdServ = new AsyncCommandServer(cmdExec, sock, controlthreads);
                Thread cmdThread = new Thread("Control Server") {
                        public void run() {
                            noti.verifyReady();
                            System.out.println("Ready control socket "+sock);
                            cmdServ.run();
                        }
                    };
                cmdThread.setDaemon(true);
                cmdThread.start();
            }
            feeder.run();
            if (display != null && Terminal.errTty()) { display.quit(); }
        } catch (Throwable th) {
            if (display != null && Terminal.errTty()) { display.quit(); }
            System.err.println(th);
            if (System.getProperty("klipspringer.debug") != null) { th.printStackTrace(); }
            System.exit(1);
        } finally {
            streamKeepAliveExecutor.exit();
        }
    }

    public static void main(String[] args) {
        try {
            new TrackPlayer().run(args);
        } catch (Throwable th) {
            th.printStackTrace();
            System.exit(1);
        }
    }
}
