use crate::{
    ffmpeg_utils::InputWrapper,
    player::{
        AUDIO_SAMPLE_RATE, PlayerCommand, PlayerState, audio, utils,
        video::{self},
    },
};
use ffmpeg::{
    frame::{Audio as AudioFrame, Video as VideoFrame},
    software::{resampling::Context as SwResampler, scaling::Context as SwScaler},
};
use std::{sync::mpsc, time::Duration};
use thiserror::Error;

#[derive(Debug, Error)]
#[error("decoder error")]
pub enum DecoderError {
    #[error("ffmpeg error: {0}")]
    FFmpeg(#[from] ffmpeg::Error),
    #[error("error: missing required pts value")]
    MissingPts,
    #[error("{0}")]
    Deque(#[from] video::ContextError),
    #[error("audio context error: {0}")]
    AudioContext(#[from] audio::ContextError),
}

#[derive(Debug, Clone, Copy)]
pub struct ColorInfo {
    /// 0 = BT.601, 1 = BT.709, 2 = BT.2020
    pub matrix_id: u32,
    /// 0 = limited/MPEG (Y: 16-235, UV: 16-240), 1 = full/JPEG (0-255)
    pub full_range: u32,
}

pub struct DecodedFrame {
    pub pts: i64,
    pub frame: ffmpeg::frame::Video,
    pub color: ColorInfo,
}

pub struct Decoder {
    video_ctx: video::SharedVideoContext,
    audio_ctx: audio::SharedAudioContext,
    audio_output_channels: u16,
    player_cmd_rx: mpsc::Receiver<PlayerCommand>,
}

impl Decoder {
    pub fn new(
        shared_video: video::SharedVideoContext,
        audio_ctx: audio::SharedAudioContext,
        audio_output_channels: u16,
        player_cmd_rx: mpsc::Receiver<PlayerCommand>,
    ) -> Self {
        Self {
            video_ctx: shared_video,
            audio_ctx,
            audio_output_channels,
            player_cmd_rx,
        }
    }

    pub fn run(&mut self, mut input: InputWrapper) -> Result<(), DecoderError> {
        let video_index = input.best_video_stream()?.index();
        let astream = input.best_audio_stream()?;
        let mut audio_index = astream.index();

        let mut vdecoder = input.video_decoder()?;
        let mut adecoder = input.audio_decoder()?;

        let color_info = {
            use ffmpeg::color::{Range, Space};
            let matrix_id = match vdecoder.color_space() {
                Space::BT709 => 1,
                Space::BT2020NCL | Space::BT2020CL => 2,
                Space::BT470BG | Space::SMPTE170M => 0,
                _ => {
                    if vdecoder.width() >= 1280 {
                        1
                    } else {
                        0
                    }
                }
            };
            let full_range = match vdecoder.color_range() {
                Range::JPEG => 1,
                _ => 0,
            };
            ColorInfo {
                matrix_id,
                full_range,
            }
        };

        let mut scaler = SwScaler::get(
            vdecoder.format(),
            vdecoder.width(),
            vdecoder.height(),
            ffmpeg::format::Pixel::YUV420P,
            vdecoder.width(),
            vdecoder.height(),
            ffmpeg::software::scaling::Flags::BILINEAR,
        )?;

        let output_channel_layout =
            ffmpeg::ChannelLayout::default(self.audio_output_channels.into());
        let output_channels = output_channel_layout.channels() as usize;
        let mut resampler = SwResampler::get(
            adecoder.format(),
            adecoder.channel_layout(),
            adecoder.rate(),
            ffmpeg::format::Sample::I16(ffmpeg::format::sample::Type::Packed),
            output_channel_layout,
            AUDIO_SAMPLE_RATE,
        )?;

        let mut audio_stream_start = astream.start_time();
        let mut audio_time_base = astream.time_base();

        let mut switch_target: Option<(f64, usize)> = None;

        let mut force_quit = false;
        'main: loop {
            let mut seek_target = None;

            'decode: for (stream, packet) in input.packets() {
                match self.player_cmd_rx.try_recv() {
                    Ok(PlayerCommand::Seek(secs)) => {
                        log::debug!("received seek comand: {secs}s");
                        seek_target = Some(secs);
                        break 'decode;
                    }
                    Ok(PlayerCommand::SwitchAudioStream { seek, index }) => {
                        log::debug!(
                            "received switch audio stream comand: stream={} seek: {:.3}",
                            index,
                            seek
                        );
                        switch_target = Some((seek, index));
                        break 'decode;
                    }
                    Err(_) => {}
                }

                if self.video_ctx.is_stopped() {
                    force_quit = true;
                    break 'main;
                }

                if stream.index() == video_index {
                    vdecoder.send_packet(&packet)?;
                    self.process_video_packet(&mut vdecoder, &mut scaler, color_info)?;
                }

                if stream.index() == audio_index {
                    adecoder.send_packet(&packet)?;
                    self.process_audio_packet(
                        audio_stream_start,
                        audio_time_base,
                        &mut adecoder,
                        &mut resampler,
                        output_channels,
                    )?;
                }
            }

            if let Some(secs) = seek_target {
                log::debug!("flushing ffmpeg a/v decoders");
                vdecoder.flush();
                adecoder.flush();
                input.seek(secs)?;
                // Clear buffers again — stale frames may have been pushed
                // between the main thread's clear and this point
                self.video_ctx.clear()?;
                self.audio_ctx.clear()?;
                // Drain any extra seek commands from slider spam
                while let Ok(_) = self.player_cmd_rx.try_recv() {}
                continue 'main;
            }

            if let Some((secs, index)) = switch_target {
                log::debug!("flushing ffmpeg a/v decoders");
                adecoder.flush();

                adecoder = input.get_audio_decoder(index)?;
                audio_index = index;

                let new_stream = input.get_audio_stream(index)?;
                audio_stream_start = new_stream.start_time();
                audio_time_base = new_stream.time_base();

                resampler = SwResampler::get(
                    adecoder.format(),
                    adecoder.channel_layout(),
                    adecoder.rate(),
                    ffmpeg::format::Sample::I16(ffmpeg::format::sample::Type::Packed),
                    output_channel_layout,
                    AUDIO_SAMPLE_RATE,
                )?;

                self.audio_ctx.clear()?;
                self.video_ctx.clear()?;

                vdecoder.flush();
                input.seek(secs)?;

                while self.player_cmd_rx.try_recv().is_ok() {}
                continue 'main;
            }

            break 'main;
        }

        if !force_quit {
            vdecoder.send_eof()?;
            self.process_video_packet(&mut vdecoder, &mut scaler, color_info)?;

            adecoder.send_eof()?;
            self.process_audio_packet(
                audio_stream_start,
                audio_time_base,
                &mut adecoder,
                &mut resampler,
                output_channels,
            )?;

            self.video_ctx.set_state(PlayerState::DecoderFinished);
        }

        log::info!("shutting down decoder");

        Ok(())
    }

    fn process_video_packet(
        &self,
        decoder: &mut ffmpeg::decoder::Video,
        scaler: &mut SwScaler,
        color: ColorInfo,
    ) -> Result<(), DecoderError> {
        let mut frame = VideoFrame::empty();
        'running: while decoder.receive_frame(&mut frame).is_ok() {
            let pts = frame.pts().ok_or(DecoderError::MissingPts)?;
            let mut scaled_frame = VideoFrame::empty();
            scaler.run(&frame, &mut scaled_frame)?;

            let mut is_full = self.video_ctx.is_full()?;
            while is_full {
                if self.video_ctx.is_stopped() {
                    break 'running;
                }
                std::thread::sleep(Duration::from_millis(10));
                is_full = self.video_ctx.is_full()?;
            }

            self.video_ctx.push_frame(DecodedFrame {
                pts,
                frame: scaled_frame,
                color,
            })?;
        }
        Ok(())
    }

    fn process_audio_packet(
        &self,
        start_time: i64,
        time_base: ffmpeg::Rational,
        decoder: &mut ffmpeg::decoder::Audio,
        resampler: &mut SwResampler,
        output_channels: usize,
    ) -> Result<(), DecoderError> {
        let mut frame = AudioFrame::empty();
        while decoder.receive_frame(&mut frame).is_ok() {
            let mut audio_ctx = self.audio_ctx.inner()?;
            let mut resampled = AudioFrame::empty();
            if let Some(delay) = resampler.run(&frame, &mut resampled)? {
                log::warn!("resample delay handling is unimplemented: {:?}", delay);
            }

            let num_samples = resampled.samples();
            let total_samples = output_channels * num_samples;
            let data = resampled.data(0);

            let samples: &[i16] =
                unsafe { std::slice::from_raw_parts(data.as_ptr() as *const i16, total_samples) };

            if !audio_ctx.clock.has_offset() {
                let pts = frame.pts().unwrap_or(0);
                let timestamp = utils::pts_to_secs(pts, start_time, time_base);
                log::debug!(
                    "[audio-clock] priming offset: pts={}, start_time={}, time_base={}/{}, offset={:.3}",
                    pts,
                    start_time,
                    time_base.0,
                    time_base.1,
                    timestamp
                );
                audio_ctx.clock.set_offset(timestamp);
            }

            audio_ctx.buffer.extend(samples);
        }

        Ok(())
    }
}
