use std::fs::File;
use std::io::{BufReader, Write, BufWriter};
use std::iter::Iterator;
use std::path::{PathBuf, StripPrefixError};

use walkdir::{IntoIter, WalkDir};

use failure::Fail;

use crate::ReadSeek;

/// A numeric ID used to refer to a [Source].
pub type SourceId = usize;

/// Trust settings for a given [Source]
#[derive(Debug, Copy, Clone, Eq, PartialEq)]
pub enum TrustLevel
{
    /// The source is untrusted (it cannot load resources that require trust)
    UntrustedSource,
    /// The source is trusted (it can load resources that require trust)
    TrustedSource
}

/// Holds a list of [Source] objects and selects one to use when loading a package
pub struct SourceManager
{
    sources : Vec<Box<dyn Source>>
}

impl SourceManager
{
    /// Constructs a new, empty [SourceManager]
    pub fn new() -> SourceManager
    {
        SourceManager
        {
            sources : Vec::new()
        }
    }
    
    /// Adds the given [Source] to the end of the list.
    pub fn add_source(&mut self, source : Box<dyn Source>) -> SourceId
    {
        self.sources.push(source);
        
        self.sources.len()
    }
    
    /// Returns a reference to the [Source] of the given ID.
    pub fn source(&mut self, id : SourceId) -> Option<&mut Box<dyn Source>>
    {
        if id == 0 || id - 1 >= self.sources.len()
        {
            return None
        }
        
        Some(&mut self.sources[id - 1])
    }
    
    /// Retrieves a mutable reference to a [Source] that has the given package, going in
    ///  reverse order of when they were added. If no suitable [Source] is available,
    ///  returns [None].
    pub fn get_package_source(&mut self, package_name : &str) -> Option<&mut Box<dyn Source>>
    {
        for source in self.sources.iter_mut().rev()
        {
            if source.has_package(package_name)
            {
                return Some(source);
            }
        }
        
        None
    }
    
    /// Removes all sources from the manager. Existing source IDs are invalidated.
    pub fn clear(&mut self)
    {
        self.sources.clear();
    }
}

/// An error returned when failing to access a package in a [Source]
#[derive(Debug, Fail)]
pub enum PackageError
{
    /// An error occurred while iterating through the packages, but no more information is available.
    ///  You should generally avoid using this unless you have to.
    #[fail(display = "Failed to access package source")]
    Generic,
    /// An error of type [std::io::Error] occurred.
    #[fail(display = "{}", _0)]
    IoError(#[cause] std::io::Error),
    /// A package item's path does not belong to the package's path.
    #[fail(display = "Package item does not belong to the given prefix: {}", _0)]
    PrefixMismatch(StripPrefixError),
    /// The operation is not supported by this implementation
    #[fail(display = "Operation not supported")]
    NotSupported
}

impl From<std::io::Error> for PackageError
{
    fn from(error : std::io::Error) -> PackageError
    {
        PackageError::IoError(error)
    }
}

/// Represents a location that packages can be loaded from. For example, you could load packages from the
///  filesystem (via [FilesystemSource], or out of a special archive format.
pub trait Source
{
    /// The path that this [Source] originates from. Only used for debug purposes.
    fn get_uri(&self) -> &str;
    /// Returns true if the [Source] has a package of the given name, otherwise returns false
    fn has_package(&self, package_name : &str) -> bool;
    /// Returns a list of all packages available in this [Source]. Do not call this repeatedly!
    fn list_packages(&mut self) -> Vec<String>;
    /// Returns a [Read] + [Seek] for the file at the given pathname, if one exists.
    fn read_file(&mut self, full_pathname : &str) -> Result<Box<dyn ReadSeek>, PackageError>;
    /// Returns a [Write] for the file at the given pathname.
    #[allow(unused_variables)]
    fn write_file(&mut self, full_pathname : &str) -> Result<Box<dyn Write>, PackageError> { Err(PackageError::NotSupported) }
    /// Returns an iterator through the items in a given package, if the [Source] has said package
    fn iter_entries(&mut self, package_name : &str, type_folder : &str) -> Box<dyn Iterator<Item = Result<String, PackageError>>>;
    /// Returns the source's trust level for the given package. Trusted sources are able to load
    ///  resource types marked as requiring trust.
    fn trust_level(&self, package_name : &str) -> TrustLevel;
}


////////////////////////////////////////////////////////////////////////////////


struct FilesystemIter
{
    basepath : PathBuf,
    walkdir : IntoIter
}

impl FilesystemIter
{
    fn new<P : Into<PathBuf>>(basepath : P) -> FilesystemIter
    {
        let path = basepath.into();
        
        FilesystemIter
        {
            basepath : path.clone(),
            walkdir : WalkDir::new(path).into_iter(),
        }
    }
}

impl Iterator for FilesystemIter
{
    type Item = Result<String, PackageError>;
    
    fn next(&mut self) -> Option<Result<String, PackageError>>
    {
        while let Some(dir_next) = self.walkdir.next()
        {
            let dir_entry = try_or_else!(dir_next,
                |error : walkdir::Error|
            {
                if let Some(io_error) = error.io_error()
                {
                    if io_error.kind() == std::io::ErrorKind::NotFound
                    {
                        return None;
                    }
                }
                
                Some(Err(PackageError::IoError(error.into())))
            });
            
            if !dir_entry.file_type().is_file()
            {
                continue;
            }
                
            let path = dir_entry.path();
            let filepath = try_or_else!(path.strip_prefix(self.basepath.clone()),
                |error| Some(Err(PackageError::PrefixMismatch(error))));
            
            let mut pathname = String::new();
            let mut first_part = true;
            
            for part in filepath.iter()
            {
                if !first_part
                {
                    pathname += "/";
                }
                pathname += &part.to_string_lossy();
                
                first_part = false;
            }
            
            return Some(Ok(pathname));
        }
        
        None
    }
}

/// A [Source] that loads packages from the filesystem. This is a good source to use for
///  development, or if you don't care about packaging your data files into an archive.
pub struct FilesystemSource
{
    basedir : String,
    package_list : Option<Vec<String>>,
    trust : TrustLevel
}

impl FilesystemSource
{
    /// Creates a new [FilesystemSource] using the given directory to look for packages in
    pub fn new(directory : &str, trust : TrustLevel) -> FilesystemSource
    {
        FilesystemSource
        {
            basedir : directory.to_string(),
            package_list : None,
            trust
        }
    }
}

impl Source for FilesystemSource
{
    fn get_uri(&self) -> &str
    {
        &self.basedir
    }
    
    fn has_package(&self, package_name : &str) -> bool
    {
        let mut path = PathBuf::from(&self.basedir);
        path.push(package_name);
        
        path.exists()
    }
    
    fn list_packages(&mut self) -> Vec<String>
    {
        if let Some(package_list) = &self.package_list
        {
            return package_list.clone();
        }
        
        let mut package_list : Vec<String> = Vec::new();
        let path = PathBuf::from(&self.basedir);
        
        let walkdir = WalkDir::new(path).min_depth(1).max_depth(1).into_iter();
        
        for dir_entry in walkdir
        {
            if let Ok(entry) = dir_entry
            {
                if !entry.file_type().is_dir()
                {
                    continue;
                }
                    
                package_list.push(entry.file_name().to_string_lossy().to_string());
            }
        }
        
        package_list.sort();
        
        self.package_list = Some(package_list.clone());
        
        package_list
    }
    
    fn read_file(&mut self, full_pathname : &str) -> Result<Box<dyn ReadSeek>, PackageError>
    {
        let mut path = PathBuf::from(&self.basedir);
        path.push(full_pathname);
        
        let file = BufReader::new(try_or_else!(File::open(path),
            |error| Err(PackageError::IoError(error))));
        
        Ok(Box::new(file))
    }

    fn write_file(&mut self, full_pathname : &str) -> Result<Box<dyn Write>, PackageError>
    {
        let mut path = PathBuf::from(&self.basedir);
        path.push(full_pathname);
        let mut folder_path = path.clone();

        if !full_pathname.ends_with("/")
        {
            folder_path.pop();
        }

        try_or_else!(std::fs::create_dir_all(folder_path),
            |error| Err(PackageError::IoError(error)));

        let file = BufWriter::new(try_or_else!(File::create(path),
            |error| Err(PackageError::IoError(error))));

        Ok(Box::new(file))
    }
    
    fn iter_entries(&mut self, package_name : &str, type_folder : &str) -> Box<dyn Iterator<Item = Result<String, PackageError>>>
    {
        let mut path = PathBuf::from(&self.basedir);
        path.push(package_name);
        path.push(type_folder);
        
        Box::new(FilesystemIter::new(path))
    }

    fn trust_level(&self, _package_name: &str) -> TrustLevel
    {
        self.trust
    }
}

