use std::fs;

use symphonia::{
    core::{
        audio::GenericAudioBufferRef,
        codecs::audio::{AudioDecoder, AudioDecoderOptions},
        errors::Error as SymphoniaError,
        formats::{FormatOptions, FormatReader, TrackType},
        io::{MediaSourceStream, MediaSourceStreamOptions},
    },
    default::formats::{AiffReader, FlacReader, 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 RawDecoder {
    pub stream: Stream,
    pub format_reader: Box<dyn FormatReader>,
    pub decoder: Box<dyn AudioDecoder>,
}

impl RawDecoder {
    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::Mpa => 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)?),
            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 = symphonia::default::get_codecs().make_audio_decoder(params, &dec_opts)?;

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

    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) =>
                {
                    return Ok(None);
                }
                Ok(None) => 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) => Ok(Some(decoded)),
                    Err(SymphoniaError::DecodeError(_)) => Ok(None),
                    Err(SymphoniaError::IoError(_)) => Ok(None),
                    Err(err) => Err(DecodingError::Symphonia(err)),
                };
            }
        }
    }
}
