use std::{fs, num::NonZero, sync::LazyLock};

pub use symphonia::core::errors::Error as SymphoniaError;
use symphonia::{
    core::{
        audio::GenericAudioBufferRef,
        codecs::{
            audio::{AudioDecoder, AudioDecoderOptions},
            registry::CodecRegistry,
        },
        errors::SeekErrorKind,
        formats::{FormatOptions, FormatReader, SeekMode, SeekTo, TrackType},
        io::{MediaSourceStream, MediaSourceStreamOptions},
        units::{Time, TimeBase},
    },
    default::formats::{AiffReader, FlacReader, IsoMp4Reader, MpaReader, OggReader, WavReader},
};

use thiserror::Error;

use crate::media_container::{ContainerError, ContainerFormat, MediaContainer, Stream};

#[derive(Debug, Error)]
pub enum DecodingError {
    #[error("{0}")]
    Io(#[from] std::io::Error),

    #[error("{0}")]
    Symphonia(#[from] SymphoniaError),

    #[error("{0}")]
    Container(#[from] ContainerError),

    #[error("Player attempted to play an unsupported container: '{0:?}'")]
    UnsupportedContainer(ContainerFormat),
}

pub struct Decoder {
    pub stream: Stream,
    pub format_reader: Box<dyn FormatReader>,
    pub decoder: Box<dyn AudioDecoder>,

    pub current_frame: usize,
    pub decoded_frames: usize,
    pub at_eof: bool,
}

static CODEC_REGISTRY: LazyLock<CodecRegistry> = LazyLock::new(|| {
    let mut codec_registry = CodecRegistry::new();
    symphonia::default::register_enabled_codecs(&mut codec_registry);
    #[cfg(feature = "opus")]
    codec_registry.register_audio_decoder::<symphonia_adapter_libopus::OpusDecoder>();
    codec_registry
});

impl Decoder {
    pub fn from_container(
        container: &MediaContainer,
        buffer_len: usize,
    ) -> Result<Self, DecodingError> {
        let file = fs::File::open(container.path())?;

        let options = MediaSourceStreamOptions { buffer_len };
        let mss = MediaSourceStream::new(Box::new(file), options);

        let fmt_opts = FormatOptions::default();
        let format_reader: Box<dyn FormatReader> = match container.format() {
            ContainerFormat::Flac => Box::new(FlacReader::try_new(mss, fmt_opts)?),
            ContainerFormat::Mp3 => Box::new(MpaReader::try_new(mss, fmt_opts)?),
            ContainerFormat::Ogg => Box::new(OggReader::try_new(mss, fmt_opts)?),
            ContainerFormat::Wav => Box::new(WavReader::try_new(mss, fmt_opts)?),
            ContainerFormat::Aiff => Box::new(AiffReader::try_new(mss, fmt_opts)?),
            ContainerFormat::Mp4 => Box::new(IsoMp4Reader::try_new(mss, fmt_opts)?),
            other => return Err(DecodingError::UnsupportedContainer(*other)),
        };

        let track = format_reader
            .default_track(TrackType::Audio)
            .expect("Playable file does not have any supported codecs");

        let dec_opts: AudioDecoderOptions = Default::default();

        let params = track
            .codec_params
            .as_ref()
            .expect("Track is not playable")
            .audio()
            .unwrap();

        let decoder = CODEC_REGISTRY.make_audio_decoder(params, &dec_opts)?;

        Ok(Self {
            stream: Stream::try_from(track)?,
            format_reader,
            decoder,

            current_frame: 0,
            decoded_frames: 0,
            at_eof: false,
        })
    }

    pub fn decode_next_packet(
        &mut self,
    ) -> Result<Option<GenericAudioBufferRef<'_>>, DecodingError> {
        loop {
            let packet = match self.format_reader.next_packet() {
                Ok(Some(packet)) => packet,
                Err(SymphoniaError::ResetRequired) => {
                    self.decoder.reset();
                    continue;
                }
                Err(SymphoniaError::IoError(err))
                    if matches!(err.kind(), std::io::ErrorKind::UnexpectedEof) =>
                {
                    self.at_eof = true;
                    return Ok(None);
                }
                Ok(None) => {
                    self.at_eof = true;
                    return Ok(None);
                }
                Err(err) => return Err(DecodingError::Symphonia(err)),
            };

            while !self.format_reader.metadata().is_latest() {
                self.format_reader.metadata().pop();
            }

            if packet.track_id == self.stream.id {
                return match self.decoder.decode(&packet) {
                    Ok(decoded) => {
                        self.current_frame += decoded.frames();
                        self.decoded_frames += decoded.frames();
                        Ok(Some(decoded))
                    }
                    Err(SymphoniaError::DecodeError(_)) => Ok(None),
                    Err(SymphoniaError::IoError(_)) => Ok(None),
                    Err(err) => Err(DecodingError::Symphonia(err)),
                };
            }
        }
    }
}

impl Decoder {
    #[must_use]
    /// Returns the current time within the track
    pub fn time(&self) -> f64 {
        let sample_rate = f64::from(self.stream.codec_params.sample_rate);
        self.current_frame as f64 / sample_rate
    }

    #[must_use]
    /// Returns the amount of 'time' that has been decoded total, regardless of seeking
    pub fn decoded_time(&self) -> f64 {
        let sample_rate = f64::from(self.stream.codec_params.sample_rate);
        self.decoded_frames as f64 / sample_rate
    }

    pub fn seek(&mut self, seconds: f64, increment: bool) -> Result<f64, SymphoniaError> {
        let seconds = if increment {
            (self.time() + seconds).max(0.0)
        } else {
            seconds
        }
        .max(0.0);

        let timestamp = Time::try_from_secs_f64(seconds).unwrap();

        let seeked_to = match self.format_reader.seek(
            SeekMode::Accurate,
            SeekTo::Time {
                time: timestamp,
                track_id: Some(self.stream.id),
            },
        ) {
            Ok(seeked_to) => seeked_to,
            Err(SymphoniaError::SeekError(SeekErrorKind::OutOfRange)) => {
                self.at_eof = true;
                return Ok(self.stream.duration());
            }
            Err(other) => return Err(other),
        };

        self.decoder.reset();
        self.at_eof = false;

        let time_base = self
            .stream
            .time_base
            .map(|(n, d)| TimeBase {
                numer: NonZero::new(n).unwrap(),
                denom: NonZero::new(d).unwrap(),
            })
            .unwrap();

        let raw_time = time_base.calc_time(seeked_to.actual_ts).unwrap();
        let time = raw_time.as_secs_f64();

        let sample_rate = self.stream.codec_params.sample_rate;
        self.current_frame = (time * f64::from(sample_rate)) as usize;

        Ok(time)
    }
}
