use std::{fs::OpenOptions, path::Path, process::Stdio};

use tokio::process::{Child, Command};

use crate::error::AppError;
use crate::media::playback::PlaybackPlan;

const SEEK_PREROLL_SECONDS: f64 = 3.0;

pub fn spawn(
    ffmpeg: &Path,
    media: &Path,
    directory: &Path,
    plan: &PlaybackPlan,
    start_seconds: f64,
) -> Result<Child, AppError> {
    let log = OpenOptions::new()
        .create(true)
        .truncate(true)
        .write(true)
        .open(directory.join("ffmpeg.log"))
        .map_err(AppError::internal)?;
    let mut command = Command::new(ffmpeg);
    command.current_dir(directory).args(["-v", "warning"]);
    let (input_seek, output_trim) = seek_offsets(start_seconds);
    if input_seek > 0.0 {
        command.args(["-ss", &format!("{input_seek:.3}")]);
    }
    command
        // Build a small lead over playback so transient encoder, disk or network
        // jitter does not leave the player waiting at the live edge.
        .args(["-readrate", "1.15"]);
    if output_trim > 0.0 {
        command.args(["-readrate_initial_burst", &format!("{output_trim:.3}")]);
    }
    command.args(["-i"]).arg(media);
    if output_trim > 0.0 {
        command.args(["-ss", &format!("{output_trim:.3}")]);
    }
    command
        .args(plan.ffmpeg_output_args())
        .stdin(Stdio::null())
        .stdout(Stdio::null())
        .stderr(Stdio::from(log))
        .kill_on_drop(true)
        .spawn()
        .map_err(AppError::internal)
}

fn seek_offsets(start_seconds: f64) -> (f64, f64) {
    let input_seek = (start_seconds - SEEK_PREROLL_SECONDS).max(0.0);
    (input_seek, start_seconds - input_seek)
}

#[cfg(test)]
mod tests {
    use super::seek_offsets;

    #[test]
    fn seek_keeps_a_short_accurate_preroll() {
        assert_eq!(seek_offsets(0.0), (0.0, 0.0));
        assert_eq!(seek_offsets(2.0), (0.0, 2.0));
        assert_eq!(seek_offsets(100.0), (97.0, 3.0));
    }
}
