use crate::media::{TrackId, TrackInfo};
use id3::{Tag, TagLike};
use std::{
    fs::{DirEntry, ReadDir},
    path::{Path, PathBuf},
};
use thiserror::Error;

#[derive(Debug, Error)]
pub enum ScannerError {
    #[error("Cannot scan file")]
    CannotScan,
    #[error("File not found")]
    FileNotFound,
    #[error("Tag not found")]
    TagNotFound,
    #[error("IO error {0}")]
    IO(std::io::Error),
}

impl From<std::io::Error> for ScannerError {
    fn from(err: std::io::Error) -> Self {
        Self::IO(err)
    }
}

impl From<id3::Error> for ScannerError {
    fn from(err: id3::Error) -> ScannerError {
        match err.kind {
            id3::ErrorKind::Io(err) => ScannerError::IO(err),
            id3::ErrorKind::StringDecoding(_) => ScannerError::CannotScan,
            id3::ErrorKind::NoTag => ScannerError::TagNotFound,
            id3::ErrorKind::Parsing => ScannerError::CannotScan,
            id3::ErrorKind::InvalidInput => ScannerError::CannotScan,
            id3::ErrorKind::UnsupportedFeature => ScannerError::CannotScan,
        }
    }
}

pub trait MusicScanner: Sync + Send {
    fn scan<'a>(&'a self) -> Box<dyn Iterator<Item = Result<TrackInfo, ScannerError>> + 'a>;
}

pub struct FileScanner {
    roots: Vec<PathBuf>,
}

impl FileScanner {
    pub fn new(roots: Vec<PathBuf>) -> Self {
        Self { roots }
    }
}

pub struct FileIterator {
    dirs: Vec<PathBuf>,
    file_iter: Option<ReadDir>,
}

impl FileIterator {
    fn scan_file(&self, path: PathBuf) -> Result<TrackInfo, ScannerError> {
        let mimetype = mime_guess::from_path(path.clone())
            .first()
            .ok_or(ScannerError::CannotScan)?;
        match (mimetype.type_(), mimetype.subtype().as_str()) {
            (mime::AUDIO, "mpeg") => TrackInfo::scan_id3(path, mimetype),

            /*
            (mime::AUDIO, "ogg") => Ok(TrackInfo {
                id: TrackId::from(path.to_str().unwrap().to_owned()),
                album: None,
                artist: None,
                name: path
                    .file_stem()
                    .and_then(|s| s.to_str())
                    .map(|s| s.to_owned()),
                track_number: None,
                filetype: mimetype,
            }),
            (mime::AUDIO, "flac") => Ok(TrackInfo {
                id: TrackId::from(path.to_str().unwrap().to_owned()),
                album: None,
                artist: None,
                name: path
                    .file_stem()
                    .and_then(|s| s.to_str())
                    .map(|s| s.to_owned()),
                track_number: None,
                filetype: mimetype,
            }),
            */
            _ => Err(ScannerError::CannotScan),
        }
    }
}

impl TrackInfo {
    fn scan_id3(path: PathBuf, mimetype: mime::Mime) -> Result<TrackInfo, ScannerError> {
        let tags = Tag::read_from_path(path.clone()).map_err(ScannerError::from)?;

        Ok(TrackInfo {
            id: TrackId::from(path.to_str().unwrap().to_owned()),
            album: tags.album().map(|s| s.to_owned()),
            artist: tags.artist().map(|s| s.to_owned()),
            name: tags.title().map(|s| s.to_owned()).or_else(|| {
                path.file_stem()
                    .and_then(|s| s.to_str())
                    .map(|s| s.to_owned())
            }),
            duration: tags.duration(),
            track_number: None,
            filetype: mimetype,
        })
    }
}

enum EntryInfo {
    Dir(PathBuf),
    File(PathBuf),
}

impl Iterator for FileIterator {
    type Item = Result<TrackInfo, ScannerError>;

    fn next(&mut self) -> Option<Self::Item> {
        fn process_entry(entry: DirEntry) -> Result<EntryInfo, ScannerError> {
            entry
                .metadata()
                .map_err(ScannerError::from)
                .map(|metadata| {
                    if metadata.is_dir() {
                        EntryInfo::Dir(entry.path())
                    } else {
                        EntryInfo::File(entry.path())
                    }
                })
        }

        let next_file = match &mut self.file_iter {
            Some(iter) => iter.next(),
            None => None,
        };

        match next_file {
            Some(Ok(entry)) => match process_entry(entry) {
                Ok(EntryInfo::Dir(path)) => {
                    self.dirs.push(path);
                    self.next()
                }
                Ok(EntryInfo::File(path)) => Some(self.scan_file(path)),
                Err(err) => Some(Err(err)),
            },
            Some(Err(err)) => Some(Err(ScannerError::from(err))),
            None => match self.dirs.pop() {
                Some(dir) => match dir.read_dir() {
                    Ok(entry) => {
                        self.file_iter = Some(entry);
                        self.next()
                    }
                    Err(err) => {
                        if err.kind() == std::io::ErrorKind::NotFound {
                            Some(Err(ScannerError::FileNotFound))
                        } else {
                            Some(Err(ScannerError::from(err)))
                        }
                    }
                },
                None => None,
            },
        }
    }
}

impl MusicScanner for FileScanner {
    fn scan<'a>(&'a self) -> Box<dyn Iterator<Item = Result<TrackInfo, ScannerError>> + 'a> {
        Box::new(FileIterator {
            dirs: self.roots.clone(),
            file_iter: None,
        })
    }
}

#[cfg(test)]
pub mod factories {
    use super::*;
    use crate::media::TrackId;

    pub struct MockScanner {
        data: Vec<TrackInfo>,
    }

    impl MockScanner {
        pub fn new() -> Self {
            Self {
                data: vec![
                    TrackInfo {
                        id: TrackId::from("/home/savanni/Track 1.mp3".to_owned()),
                        track_number: Some(1),
                        name: Some("Track 1".to_owned()),
                        album: Some("Savanni's Demo".to_owned()),
                        artist: Some("Savanni".to_owned()),
                        duration: Some(15),
                        filetype: "audio/mpeg".parse::<mime::Mime>().unwrap(),
                    },
                    TrackInfo {
                        id: TrackId::from("/home/savanni/Track 2.mp3".to_owned()),
                        track_number: Some(2),
                        name: Some("Track 2".to_owned()),
                        album: Some("Savanni's Demo".to_owned()),
                        artist: Some("Savanni".to_owned()),
                        duration: Some(15),
                        filetype: "audio/mpeg".parse::<mime::Mime>().unwrap(),
                    },
                    TrackInfo {
                        id: TrackId::from("/home/savanni/Track 3.mp3".to_owned()),
                        track_number: Some(3),
                        name: Some("Track 3".to_owned()),
                        album: Some("Savanni's Demo".to_owned()),
                        artist: Some("Savanni".to_owned()),
                        duration: Some(15),
                        filetype: "audio/mpeg".parse::<mime::Mime>().unwrap(),
                    },
                    TrackInfo {
                        id: TrackId::from("/home/savanni/Track 4.mp3".to_owned()),
                        track_number: Some(4),
                        name: Some("Track 4".to_owned()),
                        album: Some("Savanni's Demo".to_owned()),
                        artist: Some("Savanni".to_owned()),
                        duration: Some(15),
                        filetype: "audio/mpeg".parse::<mime::Mime>().unwrap(),
                    },
                    TrackInfo {
                        id: TrackId::from("/home/savanni/Track 5.mp3".to_owned()),
                        track_number: Some(5),
                        name: Some("Track 5".to_owned()),
                        album: Some("Savanni's Demo".to_owned()),
                        artist: Some("Savanni".to_owned()),
                        duration: Some(15),
                        filetype: "audio/mpeg".parse::<mime::Mime>().unwrap(),
                    },
                ],
            }
        }
    }

    impl MusicScanner for MockScanner {
        fn scan<'a>(&'a self) -> Box<dyn Iterator<Item = Result<TrackInfo, ScannerError>> + 'a> {
            Box::new(self.data.iter().map(|t| Ok(t.clone())))
        }
    }
}
