mod audio;
mod decoder;
pub mod error;
pub use error::PlayerError;
mod utils;
mod video;

pub use decoder::ColorInfo;
pub use video::ContextError as VideoContextError;

use crate::{
    ffmpeg_utils::{AudioStreamInfo, InputWrapper},
    player::{audio::SharedAudioContext, decoder::Decoder, video::SharedVideoContext},
};
use std::{fmt, sync::mpsc};

pub const AUDIO_BUFFER_SIZE: u16 = 4096;
/// Audio in video files is essentially always 48 kHz, so this makes the common
/// case a passthrough instead of running every frame through a rate conversion.
pub const AUDIO_SAMPLE_RATE: u32 = 48000;
pub const VIDEO_FRAME_BUFFER_SIZE: usize = 32;
pub const TIME_SYNC_THRESHOLD: f64 = 0.01;
pub const FRAME_DELAY: f64 = 0.001;

#[derive(Debug, Clone, Copy)]
pub enum PlayerCommand {
    Seek(f64),
    SwitchAudioStream { seek: f64, index: usize },
}

impl fmt::Display for PlayerCommand {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            PlayerCommand::Seek(sec) => write!(f, "seek to {sec} seconds"),
            PlayerCommand::SwitchAudioStream { index, .. } => {
                write!(f, "switch to audio stream={index}")
            }
        }
    }
}

pub enum FrameAction {
    RenderFrame,
    Delay(f64),
    DropFrame,
    Nothing,
}

impl fmt::Display for FrameAction {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            FrameAction::RenderFrame => f.write_str("RenderFrame"),
            FrameAction::Delay(d) => write!(f, "Delay({d:.3})"),
            FrameAction::DropFrame => f.write_str("DropFrame"),
            FrameAction::Nothing => f.write_str("Nothing"),
        }
    }
}

#[allow(dead_code)]
pub struct MediaInfo {
    pub width: u32,
    pub height: u32,
    pub video_time_base: ffmpeg::Rational,
    pub audio_time_base: ffmpeg::Rational,
    pub video_start_time: i64,
    pub audio_start_time: i64,
    pub frame_rate: ffmpeg::Rational,
    pub frame_duration: f64,
    pub stream_offset: f64,
    pub total_duration: f64,
    pub audio_output_channels: u16,
    pub audio_streams: Vec<AudioStreamInfo>,
    pub current_audio_index: usize,
}

pub struct Player {
    pub video_ctx: SharedVideoContext,
    pub audio_ctx: SharedAudioContext,
    pub audio_output: Option<audio::AudioOutput>,
    media: MediaInfo,
    decoder_th: Option<std::thread::JoinHandle<()>>,
    player_command_tx: Option<mpsc::Sender<PlayerCommand>>,
    pub input: Option<InputWrapper>,
}

impl Player {
    pub fn new(file: &str) -> Result<Self, PlayerError> {
        let input = InputWrapper::open(file)?;
        let total_duration = input.duration_secs();
        let video_ctx = SharedVideoContext::new(VIDEO_FRAME_BUFFER_SIZE);
        let audio_ctx = SharedAudioContext::new(AUDIO_BUFFER_SIZE as u32);

        let vdecoder = input.video_decoder()?;
        let width = vdecoder.width();
        let height = vdecoder.height();
        let pixel_format = vdecoder.format();

        let video_time_base = input.video_time_base()?;
        let video_start_time = input.best_video_stream()?.start_time();
        let frame_rate = input.video_fps()?;
        let frame_duration = 1.0 / utils::rational_to_f64(frame_rate);

        let audio_streams = input.audio_streams()?;
        let current_audio_index = input.best_audio_stream()?.index();

        // Everything below describes the stream we actually decode, looked up by
        // index rather than re-deriving it from `best_audio_stream`. Taking the time
        // base or start time from a different stream than the one being decoded
        // desyncs A/V, so keep the single lookup even though they agree today.
        let selected = audio_streams
            .iter()
            .find(|s| s.index == current_audio_index)
            .ok_or(PlayerError::AudioStreamNotFound(current_audio_index))?;
        let audio_time_base = selected.time_base;
        let audio_start_time = selected.start_time;
        let source_channels = selected.channels;

        let v_start_secs = video_start_time as f64 * utils::rational_to_f64(video_time_base);
        let a_start_secs = audio_start_time as f64 * utils::rational_to_f64(audio_time_base);
        let stream_offset = v_start_secs - a_start_secs;

        let device_channels = audio::AudioOutput::device_channels().unwrap_or(2);
        let audio_output_channels = source_channels.min(device_channels).max(1);

        let media = MediaInfo {
            width,
            height,
            video_time_base,
            audio_time_base,
            video_start_time,
            audio_start_time,
            frame_rate,
            frame_duration,
            stream_offset,
            total_duration,
            audio_output_channels,
            audio_streams,
            current_audio_index,
        };

        log::info!(
            "[init]: video_start={:.3} audio_start={:.3} stream_offset={:.3} audio_source={}ch audio_device={}ch audio_output={}ch",
            v_start_secs,
            a_start_secs,
            stream_offset,
            source_channels,
            device_channels,
            audio_output_channels,
        );

        log::info!("[init][video-stream]: {width}x{height} format={pixel_format:?}");
        for s in &media.audio_streams {
            log::info!(
                "[init][audio-stream]: index={} lang={} codec={:?} channels={} rate={}Hz time_base={}/{} start_time={}{}",
                s.index,
                s.language.as_deref().unwrap_or("und"),
                s.codec_id,
                s.channels,
                s.rate,
                s.time_base.0,
                s.time_base.1,
                s.start_time,
                if s.index == media.current_audio_index {
                    " [selected]"
                } else {
                    ""
                },
            );
        }

        Ok(Self {
            video_ctx,
            audio_ctx,
            audio_output: None,
            media,
            decoder_th: None,
            player_command_tx: None,
            input: Some(input),
        })
    }

    pub fn media_info(&self) -> &MediaInfo {
        &self.media
    }

    pub fn start(&mut self) -> Result<(), PlayerError> {
        let input = self.take_input()?;
        let audio_output =
            audio::AudioOutput::new(self.audio_ctx.clone(), self.media.audio_output_channels)?;

        let (decoder_th, tx) = self.spawn_decoder(input)?;
        self.decoder_th = Some(decoder_th);
        self.player_command_tx = Some(tx);
        self.audio_output = Some(audio_output);

        Ok(())
    }

    pub fn set_volume(&self, volume: f32) {
        self.audio_ctx.set_volume(volume);
    }

    pub fn set_muted(&self, muted: bool) {
        self.audio_ctx.set_muted(muted);
    }

    pub fn take_input(&mut self) -> Result<InputWrapper, PlayerError> {
        match self.input.take() {
            Some(input) => Ok(input),
            None => Err(PlayerError::InputConsumed),
        }
    }

    /// Decides what to do with the next video frame based on A/V sync.
    /// Returns (audio_clock_time, action).
    pub fn next_frame_action(&self, pts: Option<i64>) -> Result<(f64, FrameAction), PlayerError> {
        if let Some(pts) = pts {
            let seconds =
                utils::pts_to_secs(pts, self.media.video_start_time, self.media.video_time_base)
                    + self.media.stream_offset;

            let clock = match self.audio_ctx.real_time() {
                Ok(c) => c,
                // Render frame at normal speed audio may kick in later
                Err(audio::ContextError::ClockNotStarted) => {
                    return Ok((0.0, FrameAction::RenderFrame));
                }
                Err(e) => return Err(e.into()),
            };

            let diff = seconds - clock;
            log::trace!(
                "[audio]: real_clock={:.3}, seconds={:.3}, av_diff={:.3}",
                clock,
                seconds,
                diff
            );

            if diff < -self.media.frame_duration {
                Ok((clock, FrameAction::DropFrame))
            } else if diff > TIME_SYNC_THRESHOLD {
                Ok((clock, FrameAction::Delay(FRAME_DELAY)))
            } else {
                Ok((clock, FrameAction::RenderFrame))
            }
        } else {
            Ok((0.0, FrameAction::Nothing))
        }
    }

    fn spawn_decoder(
        &self,
        input: InputWrapper,
    ) -> Result<(std::thread::JoinHandle<()>, mpsc::Sender<PlayerCommand>), PlayerError> {
        let (tx, rx) = mpsc::channel();

        let mut decoder = Decoder::new(
            self.video_ctx.clone(),
            self.audio_ctx.clone(),
            self.media.audio_output_channels,
            self.media.current_audio_index,
            rx,
        );

        let thread = std::thread::Builder::new()
            .name("mplayer-decoder".to_string())
            .spawn(move || match decoder.run(input) {
                Ok(_) => {}
                Err(e) => log::error!("decoder failed with: {e}"),
            })
            .map_err(|_| PlayerError::DecoderStart)?;

        Ok((thread, tx))
    }

    pub fn pause(&self) -> Result<(), PlayerError> {
        if let Some(ref ao) = self.audio_output {
            ao.pause()?;
        }
        self.audio_ctx.inner()?.clock.pause();
        Ok(())
    }

    pub fn resume(&self) -> Result<(), PlayerError> {
        if let Some(ref ao) = self.audio_output {
            ao.resume()?;
        }
        self.audio_ctx.inner()?.clock.resume();
        Ok(())
    }

    pub fn seek(&self, secs: f64) -> Result<(), PlayerError> {
        if let Some(ref ao) = self.audio_output {
            ao.pause()?;
        }
        self.flush_buffers()?;
        if let Some(ref tx) = self.player_command_tx {
            tx.send(PlayerCommand::Seek(secs)).ok();
        }
        if let Some(ref ao) = self.audio_output {
            ao.resume()?;
        }
        Ok(())
    }

    pub fn switch_audio_stream(&mut self, stream_index: usize) -> Result<(), PlayerError> {
        let stream_info = self
            .media
            .audio_streams
            .iter()
            .find(|s| s.index == stream_index)
            .ok_or(PlayerError::AudioStreamNotFound(stream_index))?;

        let new_time_base = stream_info.time_base;
        let new_start_time = stream_info.start_time;

        let current_time = self.audio_ctx.real_time().unwrap_or(0.0);
        self.pause()?;
        self.flush_buffers()?;
        if let Some(sender) = &self.player_command_tx {
            sender
                .send(PlayerCommand::SwitchAudioStream {
                    seek: current_time,
                    index: stream_index,
                })
                .ok();
        }

        let a_start_secs = new_start_time as f64 * utils::rational_to_f64(new_time_base);
        let v_start_secs =
            self.media.video_start_time as f64 * utils::rational_to_f64(self.media.video_time_base);

        self.media.audio_time_base = new_time_base;
        self.media.audio_start_time = new_start_time;
        self.media.stream_offset = v_start_secs - a_start_secs;
        self.media.current_audio_index = stream_index;

        self.resume()?;

        Ok(())
    }

    /// Clears video and audio buffers without resetting the audio clock.
    /// Used by the main thread during seek to unblock the decoder and silence audio.
    pub fn flush_buffers(&self) -> Result<(), PlayerError> {
        self.video_ctx.clear()?;
        self.audio_ctx.clear_buffer()?;
        Ok(())
    }
}

impl Drop for Player {
    fn drop(&mut self) {
        // Signal the decoder thread to stop
        self.video_ctx.set_state(PlayerState::Stopped);

        // Drop the command channel so the decoder sees a disconnected sender
        self.player_command_tx.take();

        // Wait for the decoder thread to finish
        if let Some(th) = self.decoder_th.take() {
            log::info!("waiting for decoder thread to shut down");
            if let Err(e) = th.join() {
                log::error!("decoder thread panicked: {e:?}");
            }
        }

        // AudioOutput (CPAL stream) is dropped automatically here
        log::info!("player resources released");
    }
}

/// `PlayerState` represents the state of a player.
pub enum PlayerState {
    /// Everything is initialized and ready to play
    Init = 0,
    /// The player is currently playing
    Playing = 1,
    /// The player has stopped playing
    Stopped = 2,
    /// The player has finished playing
    DecoderFinished = 3,
}
