//! Has all of the structs for managing the state
//! of tracks, as well as downloading them & finding new ones.
//!
//! There are several structs which represent the different stages
//! that go on in downloading and playing tracks. When first queued,
//! the downloader will return a [`Queued`] track.
//!
//! Then, when it's time to play the track, it is decoded into
//! a [`Decoded`] track, which includes all the information
//! in the form of [`Info`].

use std::{fmt::Debug, io::Cursor, time::Duration};

use bytes::Bytes;
use rodio::{Decoder, Source as _};
use unicode_segmentation::UnicodeSegmentation as _;

pub mod list;
pub use list::List;
pub mod error;
pub mod format;
pub use error::{Error, Result};

use crate::tracks::error::WithTrackContext as _;

/// Just a shorthand for a decoded [Bytes].
pub type DecodedData = Decoder<Cursor<Bytes>>;

/// Tracks which are still waiting in the queue, and can't be played yet.
///
/// This means that only the data & track name are included.
#[derive(PartialEq, Eq)]
pub struct Queued {
    /// Display name of the track.
    pub display: String,

    /// Full downloadable path/url of the track.
    pub path: String,

    /// The raw data of the track, which is not decoded and
    /// therefore much more memory efficient.
    pub data: Bytes,
}

impl Debug for Queued {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("Queued")
            .field("display", &self.display)
            .field("path", &self.path)
            .field("data", &self.data.len())
            .finish()
    }
}

impl Queued {
    /// This will actually decode and format the track,
    /// returning a [`DecodedTrack`] which can be played
    /// and also has a duration & formatted name.
    pub fn decode(self) -> Result<Decoded> {
        Decoded::new(self)
    }

    /// Creates a new queued track.
    pub fn new(path: String, data: Bytes, display: Option<String>) -> Result<Self> {
        let display = match display {
            None => self::format::name(&path)?,
            Some(custom) => custom,
        };

        Ok(Self {
            display,
            path,
            data,
        })
    }
}

/// The [`Info`] struct, which has the name and duration of a track.
///
/// This is not included in [Track] as the duration has to be acquired
/// from the decoded data and not from the raw data.
#[derive(Debug, Eq, PartialEq, Clone)]
pub struct Info {
    /// The full downloadable path/url of the track.
    pub path: String,

    /// This is a formatted name, so it doesn't include the full path.
    pub display: String,

    /// This is the *actual* terminal width of the track name, used to make
    /// the UI consistent.
    pub width: usize,

    /// The duration of the track, this is an [Option] because there are
    /// cases where the duration of a track is unknown.
    pub duration: Option<Duration>,
}

impl Info {
    /// Converts the info back into a full track list entry.
    pub fn to_entry(&self) -> String {
        let mut entry = self.path.clone();
        entry.push('!');
        entry.push_str(&self.display);

        entry
    }

    /// Creates a new [`Info`] from decoded data & the queued track.
    pub fn new(decoded: &DecodedData, path: String, display: String) -> Result<Self> {
        Ok(Self {
            duration: decoded.total_duration(),
            width: display.graphemes(true).count(),
            path,
            display,
        })
    }
}

/// This struct is separate from [Track] since it is generated lazily from
/// a track, and not when the track is first downloaded.
pub struct Decoded {
    /// Has both the formatted name and some information from the decoded data.
    pub info: Info,

    /// The decoded data, which is able to be played by [rodio].
    pub data: DecodedData,
}

impl Decoded {
    /// Creates a new track.
    /// This is equivalent to [`QueuedTrack::decode`].
    pub fn new(track: Queued) -> Result<Self> {
        let (path, display) = (track.path.clone(), track.display.clone());
        let data = Decoder::builder()
            .with_byte_len(track.data.len().try_into()?)
            .with_data(Cursor::new(track.data))
            .build()
            .track(track.display)?;

        let info = Info::new(&data, path, display)?;
        Ok(Self { info, data })
    }
}
