use std::{
    fmt::Display,
    io::{Read, Write},
    mem::replace,
    path::PathBuf,
    sync::{Arc, Mutex},
    thread::JoinHandle,
    time::Instant,
};

use colorize::AnsiColor;

use crate::load::ToFromBytes;

use super::{
    database::{ClientIo, Database},
    AlbumId, ArtistId, CoverId, DatabaseLocation, GeneralData, SongId,
};

#[derive(Clone, Debug, PartialEq)]
pub struct Song {
    pub id: SongId,
    pub location: DatabaseLocation,
    pub file_last_modified_unix_timestamp: Option<u64>,
    pub title: String,
    pub album: Option<AlbumId>,
    pub artist: ArtistId,
    pub more_artists: Vec<ArtistId>,
    pub cover: Option<CoverId>,
    pub file_size: u64,
    /// song duration in milliseconds
    pub duration_millis: u64,
    pub general: GeneralData,
    /// None => No cached data
    /// Some(Err) => No cached data yet, but a thread is working on loading it.
    /// Some(Ok(data)) => Cached data is available.
    pub cached_data: CachedData,
}
impl Song {
    pub fn new(
        location: DatabaseLocation,
        file_last_modified_unix_timestamp: Option<u64>,
        title: String,
        album: Option<AlbumId>,
        artist: ArtistId,
        more_artists: Vec<ArtistId>,
        cover: Option<CoverId>,
        file_size: u64,
        duration_millis: u64,
        general: GeneralData,
    ) -> Self {
        Self {
            id: 0,
            location,
            file_last_modified_unix_timestamp,
            title,
            album,
            artist,
            more_artists,
            cover,
            file_size,
            duration_millis,
            general,
            cached_data: CachedData(Arc::new(Mutex::new((Err(None), None)))),
        }
    }

    pub fn cached_data(&self) -> &CachedData {
        &self.cached_data
    }
}
impl CachedData {
    pub fn uncache_data(&self) -> Result<bool, ()> {
        let mut cached = self.0.lock().unwrap();
        match replace(&mut cached.0, Err(None)) {
            Ok(Ok(_data)) => Ok(true),
            Ok(Err(thread)) => {
                if thread.is_finished() {
                    // get value from thread and drop it
                    _ = thread.join();
                    Ok(true)
                } else {
                    // thread is still running...
                    cached.0 = Ok(Err(thread));
                    Err(())
                }
            }
            Err(e) => {
                cached.0 = Err(e);
                Ok(false)
            }
        }
    }
    /// If no data is cached yet and no caching thread is running, starts a thread to cache the data.
    pub fn cache_data_start_thread(&self, db: &Database, song: &Song) -> bool {
        self.cache_data_start_thread_or_say_already_running(db, song) == Ok(true)
    }
    /// Ok(true) => thread started,
    /// Ok(false) => data was already loaded
    pub fn cache_data_start_thread_or_say_already_running(
        &self,
        db: &Database,
        song: &Song,
    ) -> Result<bool, bool> {
        self.get_data_or_start_thread_and_say_already_running(db, |_| false, || true, song)
    }
    /// gets the data if available, or, if no thread is running, starts a thread to get the data.
    /// if a thread is running, was started, or recently encountered an error, `None` is returned, otherwise `Some(data)`.
    pub fn get_data_or_maybe_start_thread(
        &self,
        db: &Database,
        song: &Song,
    ) -> Option<Arc<Vec<u8>>> {
        self.get_data_or_start_thread_and_say_already_running(
            db,
            |data| Some(Arc::clone(data)),
            || None,
            song,
        )
        .ok()
        .and_then(|v| v)
    }
    /// `Err(true)` if a thread is already running,
    /// `Ok(get_data(data))` if there is data,
    /// `Ok(started())` if a thread was started,
    /// `Err(false)` otherwise (i.e. loading data failed recently, 60 second cooldown between retries is active).
    pub fn get_data_or_start_thread_and_say_already_running<T>(
        &self,
        db: &Database,
        get_data: impl FnOnce(&Arc<Vec<u8>>) -> T,
        started: impl FnOnce() -> T,
        song: &Song,
    ) -> Result<T, bool> {
        let mut cd = self.0.lock().unwrap();
        match cd.0.as_mut() {
            Err(Some(i)) if i.elapsed().as_secs_f32() < 60.0 => return Err(false),
            Err(_) => (),
            Ok(Err(t)) => {
                if t.is_finished() {
                    if let Some(bytes) = replace(&mut cd.0, Err(None))
                        .unwrap()
                        .unwrap_err()
                        .join()
                        .unwrap()
                    {
                        cd.0 = Ok(Ok(bytes));
                        return Ok(get_data(cd.0.as_ref().unwrap().as_ref().unwrap()));
                    } else {
                        cd.0 = Err(Some(Instant::now()));
                        return Err(false);
                    }
                } else {
                    return Err(true);
                }
            }
            Ok(Ok(bytes)) => return Ok(get_data(&*bytes)),
        };
        let src = if let Some(dlcon) = &db.remote_server_as_song_file_source {
            Err((song.id, Arc::clone(dlcon)))
        } else {
            Ok(db.get_path(&song.location))
        };
        cd.0 = Ok(Err(std::thread::spawn(move || {
            let data = Self::load_data(src)?;
            Some(Arc::new(data))
        })));
        Ok(started())
    }
    /// If the song's data is cached, returns the number of bytes.
    pub fn has_cached_data(&self) -> Option<usize> {
        if let Ok(Ok(v)) = self.0.lock().unwrap().0.as_ref() {
            Some(v.len())
        } else {
            None
        }
    }
    /// Gets the cached data, if available.
    /// If a thread is running to load the data, it is not awaited.
    /// This function doesn't block.
    pub fn cached_data(&self) -> Option<Arc<Vec<u8>>> {
        if let Ok(Ok(v)) = self.0.lock().unwrap().0.as_ref() {
            Some(Arc::clone(v))
        } else {
            None
        }
    }
    /// Gets the cached data, if available.
    /// If a thread is running to load the data, it is awaited.
    /// This function blocks!
    pub fn cached_data_await(&self) -> Option<Arc<Vec<u8>>> {
        let mut cd = self.0.lock().unwrap();
        let (out, next) = match replace(&mut cd.0, Err(None)) {
            Ok(Ok(bytes)) => (Some(Arc::clone(&bytes)), Ok(Ok(bytes))),
            Ok(Err(t)) => {
                if let Some(bytes) = t.join().unwrap() {
                    (Some(Arc::clone(&bytes)), Ok(Ok(bytes)))
                } else {
                    (None, Err(Some(Instant::now())))
                }
            }
            Err(e) => (None, Err(e)),
        };
        cd.0 = next;
        out
    }
    fn load_data(
        src: Result<
            PathBuf,
            (
                SongId,
                Arc<Mutex<crate::server::get::Client<Box<dyn ClientIo>>>>,
            ),
        >,
    ) -> Option<Vec<u8>> {
        match src {
            Ok(path) => {
                eprintln!("[{}] loading song from {:?}", "INFO".cyan(), path);
                match std::fs::read(&path) {
                    Ok(v) => {
                        eprintln!("[{}] loaded song from {:?}", "INFO".green(), path);
                        Some(v)
                    }
                    Err(e) => {
                        eprintln!("[{}] error loading {:?}: {e:?}", "ERR!".red(), path);
                        None
                    }
                }
            }
            Err((id, dlcon)) => {
                eprintln!("[{}] loading song {id}", "INFO".cyan());
                match dlcon
                    .lock()
                    .unwrap()
                    .song_file(id)
                    .expect("problem with downloader connection...")
                {
                    Ok(data) => Some(data),
                    Err(e) => {
                        eprintln!("[{}] error loading song {id}: {e}", "ERR!".red());
                        None
                    }
                }
            }
        }
    }
}
impl Display for Song {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.title)?;
        match self.album {
            Some(album) => write!(f, " ({} by {} on {album})", self.id, self.artist)?,
            None => write!(f, " ({} by {})", self.id, self.artist)?,
        }
        Ok(())
    }
}

impl ToFromBytes for Song {
    fn to_bytes<T>(&self, s: &mut T) -> Result<(), std::io::Error>
    where
        T: Write,
    {
        self.id.to_bytes(s)?;
        self.location.to_bytes(s)?;
        self.file_last_modified_unix_timestamp.to_bytes(s)?;
        self.title.to_bytes(s)?;
        self.album.to_bytes(s)?;
        self.artist.to_bytes(s)?;
        self.more_artists.to_bytes(s)?;
        self.cover.to_bytes(s)?;
        self.file_size.to_bytes(s)?;
        self.duration_millis.to_bytes(s)?;
        self.general.to_bytes(s)?;
        Ok(())
    }
    fn from_bytes<T>(s: &mut T) -> Result<Self, std::io::Error>
    where
        T: Read,
    {
        Ok(Self {
            id: ToFromBytes::from_bytes(s)?,
            location: ToFromBytes::from_bytes(s)?,
            file_last_modified_unix_timestamp: ToFromBytes::from_bytes(s)?,
            title: ToFromBytes::from_bytes(s)?,
            album: ToFromBytes::from_bytes(s)?,
            artist: ToFromBytes::from_bytes(s)?,
            more_artists: ToFromBytes::from_bytes(s)?,
            cover: ToFromBytes::from_bytes(s)?,
            file_size: ToFromBytes::from_bytes(s)?,
            duration_millis: ToFromBytes::from_bytes(s)?,
            general: ToFromBytes::from_bytes(s)?,
            cached_data: CachedData(Arc::new(Mutex::new((Err(None), None)))),
        })
    }
}

#[derive(Debug)]
pub struct CachedData(
    pub  Arc<
        Mutex<(
            Result<Result<Arc<Vec<u8>>, JoinHandle<Option<Arc<Vec<u8>>>>>, Option<Instant>>,
            Option<usize>,
        )>,
    >,
);
impl PartialEq for CachedData {
    fn eq(&self, _other: &Self) -> bool {
        // for testing
        true
    }
}
impl Clone for CachedData {
    fn clone(&self) -> Self {
        Self(Arc::clone(&self.0))
    }
}
