package ctbrec.recorder;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Files;
import java.util.Arrays;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.function.Consumer;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import ctbrec.io.DevNull;
import ctbrec.io.ProcessStreamRedirector;
import ctbrec.recorder.download.ProcessExitedUncleanException;

public class FFmpeg {

    private static final Logger LOG = LoggerFactory.getLogger(FFmpeg.class);

    private static ScheduledExecutorService processOutputReader = Executors.newScheduledThreadPool(2, createThreadFactory("FFmpeg output stream reader"));

    private Process process;
    private boolean logOutput = false;
    private Consumer<Process> startCallback;
    private Consumer<Integer> exitCallback;
    private File ffmpegLog = null;
    private OutputStream ffmpegLogStream;
    private ProcessStreamRedirector stdoutRedirector;
    private ProcessStreamRedirector stderrRedirector;

    private FFmpeg() {}

    private static ThreadFactory createThreadFactory(String name) {
        return r -> {
            Thread t = new Thread(r);
            t.setName(name);
            t.setDaemon(true);
            t.setPriority(Thread.MIN_PRIORITY);
            return t;
        };
    }

    public void exec(String[] cmdline, String[] env, File executionDir) throws IOException {
        LOG.debug("FFmpeg command line: {}", Arrays.toString(cmdline));
        process = Runtime.getRuntime().exec(cmdline, env, executionDir);
        afterStart();
    }

    private void afterStart() throws IOException {
        notifyStartCallback(process);
        setupLogging();
    }

    public void shutdown(int exitCode) throws IOException {
        LOG.debug("FFmpeg exit code was {}", exitCode);
        ffmpegLogStream.flush();
        ffmpegLogStream.close();
        stdoutRedirector.setKeepGoing(false);
        stderrRedirector.setKeepGoing(false);
        notifyExitCallback(exitCode);
        if (exitCode != 1) {
            if (ffmpegLog != null && ffmpegLog.exists()) {
                Files.delete(ffmpegLog.toPath());
            }
        } else {
            throw new ProcessExitedUncleanException("FFmpeg exit code was " + exitCode);
        }
    }

    private void setupLogging() throws IOException {
        if (logOutput) {
            if (ffmpegLog == null) {
                ffmpegLog = File.createTempFile("ffmpeg_", ".log");
            }
            LOG.debug("Logging FFmpeg output to {}", ffmpegLog);
            ffmpegLog.deleteOnExit();
            ffmpegLogStream = new FileOutputStream(ffmpegLog);
        } else {
            ffmpegLogStream = new DevNull();
        }
        stdoutRedirector = new ProcessStreamRedirector(processOutputReader, process.getInputStream(), ffmpegLogStream);
        stderrRedirector = new ProcessStreamRedirector(processOutputReader, process.getErrorStream(), ffmpegLogStream);
        processOutputReader.submit(stdoutRedirector);
        processOutputReader.submit(stderrRedirector);
    }

    private void notifyStartCallback(Process process) {
        try {
            startCallback.accept(process);
        } catch(Exception e) {
            LOG.error("Exception in onStart callback", e);
        }
    }

    private void notifyExitCallback(int exitCode) {
        try {
            exitCallback.accept(exitCode);
        } catch(Exception e) {
            LOG.error("Exception in onExit callback", e);
        }
    }

    public int waitFor() throws InterruptedException {
        int exitCode = process.waitFor();
        try {
            shutdown(exitCode);
        } catch (IOException e) {
            LOG.error("Error while shutting down FFmpeg process", e);
        }
        return exitCode;
    }

    public static class Builder {
        private boolean logOutput = false;
        private File logFile;
        private Consumer<Process> startCallback;
        private Consumer<Integer> exitCallback;

        public Builder logOutput(boolean logOutput) {
            this.logOutput = logOutput;
            return this;
        }

        public Builder logFile(File logFile) {
            this.logFile = logFile;
            return this;
        }

        public Builder onStarted(Consumer<Process> callback) {
            this.startCallback = callback;
            return this;
        }

        public Builder onExit(Consumer<Integer> callback) {
            this.exitCallback = callback;
            return this;
        }

        public FFmpeg build() {
            FFmpeg instance = new FFmpeg();
            instance.logOutput = logOutput;
            instance.startCallback = startCallback != null ? startCallback : p -> {};
            instance.exitCallback = exitCallback != null ? exitCallback : exitCode -> {};
            instance.ffmpegLog = logFile;
            return instance;
        }
    }

}
