extern crate toml;
extern crate version_compare;

use std::fs;
use std::io::Error as IoError;
use std::path::PathBuf;

use failure::Fail;
use ffsend_api::file::remote_file::RemoteFile;
use self::toml::de::Error as DeError;
use self::toml::ser::Error as SerError;
use self::version_compare::{
    CompOp,
    VersionCompare,
};

use util::{print_error, print_warning};

/// The minimum supported history file version.
const VERSION_MIN: &str = "0.0.1";

/// The maximum supported history file version.
const VERSION_MAX: &str = crate_version!();

#[derive(Serialize, Deserialize)]
pub struct History {
    /// The application version the history file was built with.
    /// Used for compatability checking.
    version: Option<String>,

    /// The file history.
    files: Vec<RemoteFile>,

    /// Whether the list of files has changed.
    #[serde(skip)]
    changed: bool,

    /// An optional path to automatically save the history to.
    #[serde(skip)]
    autosave: Option<PathBuf>,
}

impl History {
    /// Construct a new history.
    /// A path may be given to automatically save the history to once changed.
    pub fn new(autosave: Option<PathBuf>) -> Self {
        let mut history = History::default();
        history.autosave = autosave;
        history
    }

    /// Load the history from the given file.
    pub fn load(path: PathBuf) -> Result<Self, LoadError> {
        // Read the file to a string
        let data = fs::read_to_string(&path)?;

        // Parse the data, set the autosave path
        let mut history: Self = toml::from_str(&data)?;
        history.autosave = Some(path);

        // Make sure the file version is supported
        if history.version.is_none() { 
            print_warning("History file has no version, ignoring");
            history.version = Some(crate_version!().into());
        } else {
            // Get the version number from the file
            let version = history.version.as_ref().unwrap();

            if let Ok(true) = VersionCompare::compare_to(version, VERSION_MIN, &CompOp::Lt) {
                print_warning("history file version is too old, ignoring");
            } else if let Ok(true) = VersionCompare::compare_to(version, VERSION_MAX, &CompOp::Gt) {
                print_warning("history file has an unknown version, ignoring");
            }
        }

        // Garbage collect
        history.gc();

        Ok(history)
    }

    /// Load the history from the given file.
    /// If the file doesn't exist, create a new empty history instance.
    ///
    /// Autosaving will be enabled, and will save to the given file path.
    pub fn load_or_new(file: PathBuf) -> Result<Self, LoadError> {
        if file.is_file() {
            Self::load(file)
        } else {
            Ok(Self::new(Some(file)))
        }
    }

    /// Save the history to the internal autosave file.
    pub fn save(&mut self) -> Result<(), SaveError> {
        // Garbage collect
        self.gc();

        // Get the path
        let path = self.autosave
            .as_ref()
            .ok_or(SaveError::NoPath)?;

        // If we have no files, remove the history file if it exists
        if self.files.is_empty() {
            if path.is_file() {
                fs::remove_file(&path)
                    .map_err(SaveError::Delete)?;
            }
            return Ok(());
        }

        // Ensure the file parnet directories are available
        if let Some(parent) = path.parent() {
            fs::create_dir_all(parent)?;
        }

        // Build the data and write to a file
        let data = toml::to_string(self)?;
        fs::write(&path, data)?;

        // There are no new changes, set the flag
        self.changed = false;

        Ok(())
    }

    /// Add the given remote file to the history.
    /// If a file with the same ID as the given file exists,
    /// the files are merged, see `RemoteFile::merge()`.
    ///
    /// If `overwrite` is set to true, the given file will overwrite
    /// properties on the existing file.
    pub fn add(&mut self, file: RemoteFile, overwrite: bool) {
        // Merge any existing file with the same ID
        {
            // Find anything to merge
            let merge_info: Vec<bool> = self.files.iter_mut()
                .filter(|f| f.id() == file.id())
                .map(|ref mut f| f.merge(&file, overwrite))
                .collect();
            let merged = !merge_info.is_empty();
            let changed = merge_info.iter().any(|i| *i);

            // Return if merged, update the changed state
            if merged {
                if changed {
                    self.changed = true;
                }
                return;
            }
        }

        // Add the file to the list
        self.files.push(file);
        self.changed = true;
    }

    /// Remove the given remote file, matched by it's file ID.
    ///
    /// If any file was removed, true is returned.
    pub fn remove(&mut self, file: &RemoteFile) -> bool {
        // Get the indices of files that have expired
        let expired_indices: Vec<usize> = self.files.iter()
            .enumerate()
            .filter(|&(_, f)| f.id() == file.id())
            .map(|(i, _)| i)
            .collect();

        // Remove these specific files
        for i in expired_indices.iter().rev() {
            self.files.remove(*i);
        }

        // Set the changed flag, and return
        if expired_indices.is_empty() {
            self.changed = true;
        }
        !expired_indices.is_empty()
    }

    /// Get all files.
    pub fn files(&self) -> &Vec<RemoteFile> {
        &self.files
    }

    /// Get a file from the history, based on the given remote file.
    /// The file ID and host will be compared against all files in this history.
    /// If multiple files exist within the history that are equal, only one is returned.
    /// If no matching file was found, `None` is returned.
    pub fn get_file(&self, file: &RemoteFile) -> Option<&RemoteFile> {
        self.files.iter().find(|f| f.id() == file.id() && f.host() == file.host())
    }

    /// Garbage collect (remove) all files that have been expired,
    /// as defined by their `expire_at` property.
    ///
    /// If the expiry property is None (thus unknown), the file will be kept.
    ///
    /// The number of exired files is returned.
    pub fn gc(&mut self) -> usize {
        // Get a list of expired files
        let expired: Vec<RemoteFile> = self.files
            .iter()
            .filter(|f| f.has_expired())
            .cloned()
            .collect();

        // Remove the files
        for f in &expired {
            self.remove(f);
        }

        // Set the changed flag
        if !expired.is_empty() {
            self.changed = true;
        }

        // Return the number of expired files
        expired.len()
    }
}

impl Drop for History {
    fn drop(&mut self) {
        // Automatically save if enabled and something was changed
        if self.autosave.is_some() && self.changed {
            // Save and report errors
            if let Err(err) = self.save() {
                print_error(
                    err.context("failed to auto save history, ignoring"),
                );
            }
        }
    }
}

impl Default for History {
    fn default() -> Self {
        Self {
            version: Some(crate_version!().into()),
            files: Vec::new(),
            changed: false,
            autosave: None,
        }
    }
}

#[derive(Debug, Fail)]
pub enum Error {
    /// An error occurred while loading the history from a file.
    #[fail(display = "failed to load history from file")]
    Load(#[cause] LoadError),

    /// An error occurred while saving the history to a file.
    #[fail(display = "failed to save history to file")]
    Save(#[cause] SaveError),
}

impl From<LoadError> for Error {
    fn from(err: LoadError) -> Self {
        Error::Load(err)
    }
}

impl From<SaveError> for Error {
    fn from(err: SaveError) -> Self {
        Error::Save(err)
    }
}

#[derive(Debug, Fail)]
pub enum LoadError {
    /// Failed to read the file contents from the given file.
    #[fail(display = "Failed to read from the history file")]
    Read(#[cause] IoError),

    /// Failed to parse the loaded file.
    #[fail(display = "Failed to parse the file contents")]
    Parse(#[cause] DeError),
}

impl From<IoError> for LoadError {
    fn from(err: IoError) -> Self {
        LoadError::Read(err)
    }
}

impl From<DeError> for LoadError {
    fn from(err: DeError) -> Self {
        LoadError::Parse(err)
    }
}

#[derive(Debug, Fail)]
pub enum SaveError {
    /// No autosave file path was present, failed to save.
    #[fail(display = "no autosave file path specified")]
    NoPath,

    /// Failed to serialize the history for saving.
    #[fail(display = "failed to serialize the history for saving")]
    Serialize(#[cause] SerError),

    /// Failed to write to the history file.
    #[fail(display = "failed to write to the history file")]
    Write(#[cause] IoError),

    /// Failed to delete the history file, which was tried because there
    /// are no history items to save.
    #[fail(display = "failed to delete history file, because history is empty")]
    Delete(#[cause] IoError),
}

impl From<SerError> for SaveError {
    fn from(err: SerError) -> Self {
        SaveError::Serialize(err)
    }
}

impl From<IoError> for SaveError {
    fn from(err: IoError) -> Self {
        SaveError::Write(err)
    }
}
