use ffmpeg::format::context::input::PacketIter;
use std::path::Path;

pub struct AudioStreamInfo {
    pub index: usize,
    pub language: Option<String>,
    pub channels: u16,
    pub rate: u32,
    #[allow(dead_code)]
    pub codec_id: ffmpeg::codec::Id,
    pub time_base: ffmpeg::Rational,
    pub start_time: i64,
}

pub struct InputWrapper {
    input: ffmpeg::format::context::Input,
}

impl InputWrapper {
    pub fn open<P>(path: P) -> Result<Self, ffmpeg::Error>
    where
        P: AsRef<Path>,
    {
        ffmpeg::log::set_level(ffmpeg::log::Level::Error);
        let input = ffmpeg::format::input(path.as_ref())?;
        Ok(Self { input })
    }

    pub fn packets(&mut self) -> PacketIter<'_> {
        self.input.packets()
    }

    pub fn audio_streams(&self) -> Result<Vec<AudioStreamInfo>, ffmpeg::Error> {
        let mut streams = Vec::new();

        for stream in self.input.streams() {
            let codec = ffmpeg::codec::context::Context::from_parameters(stream.parameters())?;
            if codec.medium() == ffmpeg::media::Type::Audio {
                let codec_id = codec.id();
                let decoder = codec.decoder();
                if let Ok(audio_decoder) = decoder.audio() {
                    let info = AudioStreamInfo {
                        index: stream.index(),
                        language: stream.metadata().get("language").map(String::from),
                        channels: audio_decoder.channels(),
                        rate: audio_decoder.rate(),
                        codec_id,
                        time_base: stream.time_base(),
                        start_time: stream.start_time(),
                    };
                    streams.push(info);
                }
            }
        }

        Ok(streams)
    }

    pub fn get_audio_decoder(&self, index: usize) -> Result<ffmpeg::decoder::Audio, ffmpeg::Error> {
        let info = self
            .audio_streams()?
            .into_iter()
            .find(|s| s.index == index)
            .ok_or(ffmpeg::Error::StreamNotFound)?;

        let stream = self
            .input
            .stream(info.index)
            .ok_or(ffmpeg::Error::StreamNotFound)?;

        let codec = ffmpeg::codec::Context::from_parameters(stream.parameters())?;
        codec.decoder().audio()
    }

    pub fn get_audio_stream(&self, index: usize) -> Result<ffmpeg::Stream<'_>, ffmpeg::Error> {
        let stream = self
            .input
            .stream(index)
            .ok_or(ffmpeg::Error::StreamNotFound)?;
        Ok(stream)
    }

    pub fn video_decoder(&self) -> Result<ffmpeg::decoder::Video, ffmpeg::Error> {
        let stream = self.best_video_stream()?;
        let codec_ctx = ffmpeg::codec::context::Context::from_parameters(stream.parameters())?;
        codec_ctx.decoder().video()
    }

    /// Superseded by [`Self::get_audio_decoder`], which honours the selected track
    /// rather than ffmpeg's best-stream pick. Kept for the analyze/metadata paths.
    #[allow(dead_code)]
    pub fn audio_decoder(&self) -> Result<ffmpeg::decoder::Audio, ffmpeg::Error> {
        let stream = self.best_audio_stream()?;
        let codec_ctx = ffmpeg::codec::context::Context::from_parameters(stream.parameters())?;
        codec_ctx.decoder().audio()
    }

    pub fn best_video_stream(&self) -> Result<ffmpeg::Stream<'_>, ffmpeg::Error> {
        let stream = self
            .input
            .streams()
            .best(ffmpeg::media::Type::Video)
            .ok_or(ffmpeg::Error::StreamNotFound)?;

        Ok(stream)
    }

    pub fn best_audio_stream(&self) -> Result<ffmpeg::Stream<'_>, ffmpeg::Error> {
        let stream = self
            .input
            .streams()
            .best(ffmpeg::media::Type::Audio)
            .ok_or(ffmpeg::Error::StreamNotFound)?;
        Ok(stream)
    }

    /// Returns the video time base rationale from the best video stream.
    /// See: [`Self::best_video_stream`].
    pub fn video_time_base(&self) -> Result<ffmpeg::Rational, ffmpeg::Error> {
        Ok(self.best_video_stream()?.time_base())
    }

    /// Returns the audio time base rationale from the best audio stream.
    /// See: [`Self::best_audio_stream`].
    ///
    /// Superseded by reading `time_base` off the selected [`AudioStreamInfo`] —
    /// the best stream is not necessarily the one being decoded.
    #[allow(dead_code)]
    pub fn audio_time_base(&self) -> Result<ffmpeg::Rational, ffmpeg::Error> {
        Ok(self.best_audio_stream()?.time_base())
    }

    pub fn video_fps(&self) -> Result<ffmpeg::Rational, ffmpeg::Error> {
        Ok(self.best_video_stream()?.avg_frame_rate())
    }

    /// Returns the total duration of the media in seconds.
    pub fn duration_secs(&self) -> f64 {
        self.input.duration() as f64 / f64::from(ffmpeg::ffi::AV_TIME_BASE)
    }

    pub fn seek(&mut self, seconds: f64) -> Result<(), ffmpeg::Error> {
        let ts = (seconds * f64::from(ffmpeg::ffi::AV_TIME_BASE)) as i64;
        self.input.seek(ts, ..ts)
    }
}

pub fn print_metadata(file: &str) -> Result<(), ffmpeg::Error> {
    let ictx = ffmpeg::format::input(file)?;
    for (k, v) in ictx.metadata().iter() {
        println!("{k}: {v}");
    }

    if let Some(stream) = ictx.streams().best(ffmpeg::media::Type::Video) {
        println!("best video stream index: {}", stream.index());
    }

    if let Some(stream) = ictx.streams().best(ffmpeg::media::Type::Audio) {
        println!("best audio stream index: {}", stream.index());
    }

    if let Some(stream) = ictx.streams().best(ffmpeg::media::Type::Subtitle) {
        println!("best subtitle stream index: {}", stream.index());
    }

    println!(
        "duration (seconds): {:.2}",
        ictx.duration() as f64 / f64::from(ffmpeg::ffi::AV_TIME_BASE)
    );

    for stream in ictx.streams() {
        let codec = ffmpeg::codec::context::Context::from_parameters(stream.parameters())?;
        let medium = match codec.medium() {
            ffmpeg::media::Type::Video => "video",
            ffmpeg::media::Type::Audio => "audio",
            ffmpeg::media::Type::Subtitle => "subtitle",
            _ => "unknown",
        };

        println!("stream index {} ({}):", stream.index(), medium);
        println!("\ttime_base: {}", stream.time_base());
        let time_base = stream.time_base();
        let start_time = stream.start_time();
        let start_time_f64 = start_time as f64 * (time_base.0 as f64 / time_base.1 as f64);
        println!("\tstart_time: {}", stream.start_time());
        println!("\tstart_time_calculated: {:.3}", start_time_f64);
        println!("\tduration (stream timebase): {}", stream.duration());
        println!(
            "\tduration (seconds): {:.2}",
            stream.duration() as f64 * f64::from(stream.time_base())
        );
        println!("\tframes: {}", stream.frames());
        println!("\tdisposition: {:?}", stream.disposition());
        println!("\tdiscard: {:?}", stream.discard());
        println!("\trate: {}", stream.rate());
        let avg_frame_rate = stream.avg_frame_rate();
        println!("\tavg_frame_rate: {}", avg_frame_rate);
        println!(
            "\tframe_duration: {:.3}",
            1.0 / (avg_frame_rate.0 as f64 / avg_frame_rate.1 as f64)
        );

        println!("\tmedium: {:?}", codec.medium());
        println!("\ttid: {:?}", codec.id());

        if codec.medium() == ffmpeg::media::Type::Video {
            if let Ok(video) = codec.decoder().video() {
                println!("\tbit_rate: {}", video.bit_rate());
                println!("\tmax_rate: {}", video.max_bit_rate());
                println!("\tdelay: {}", video.delay());
                println!("\tvideo.time_base: {}", video.time_base());
                println!("\tvideo.width: {}", video.width());
                println!("\tvideo.height: {}", video.height());
                println!("\tvideo.format: {:?}", video.format());
                println!("\tvideo.has_b_frames: {}", video.has_b_frames());
                println!("\tvideo.aspect_ratio: {}", video.aspect_ratio());
                println!("\tvideo.color_space: {:?}", video.color_space());
                println!("\tvideo.color_range: {:?}", video.color_range());
                println!("\tvideo.color_primaries: {:?}", video.color_primaries());
                println!(
                    "\tvideo.color_transfer_characteristic: {:?}",
                    video.color_transfer_characteristic()
                );
                println!("\tvideo.chroma_location: {:?}", video.chroma_location());
                println!("\tvideo.references: {}", video.references());
                println!("\tvideo.intra_dc_precision: {}", video.intra_dc_precision());
            }
        } else if codec.medium() == ffmpeg::media::Type::Audio {
            if let Ok(audio) = codec.decoder().audio() {
                println!("\tbit_rate: {}", audio.bit_rate());
                println!("\tmax_rate: {}", audio.max_bit_rate());
                println!("\tdelay: {}", audio.delay());
                println!("\taudio.time_base: {}", audio.time_base());
                println!("\taudio.rate: {}", audio.rate());
                println!("\taudio.channels: {}", audio.channels());
                println!("\taudio.format: {:?}", audio.format());
                println!("\taudio.frames: {}", audio.frames());
                println!("\taudio.align: {}", audio.align());
                println!("\taudio.channel_layout: {:?}", audio.channel_layout());
            }
        }
    }

    Ok(())
}
