//! This module and submodules deal with data storage and catalogs. A database
//! is a set of journals.

pub mod catalog;
pub mod journal;

use database::catalog::{Catalog, CatalogAddExtensionError, CatalogAddJournalError, CatalogInitializeError, CatalogOpenError};
use database::journal::{ConcurrentJournal, ConcurrentJournalOpenError};
use extension::{Extension, ExtensionResolveError};
use extension::dl::Dl;
use utility::path::push_path;
use utility::rc_vec::RcVec;

use std::collections::HashMap;
use std::ffi::OsString;
use std::io;
use std::os::unix::ffi::OsStringExt;
use std::path::{Path, PathBuf};
use std::sync::{Arc, RwLock};

use uuid::Uuid;

aggregate_errors!(
    DatabaseOpenError {
        CatalogOpenError(CatalogOpenError),
        ConcurrentJournalOpenError(ConcurrentJournalOpenError),
        DatabaseLoadError(DatabaseLoadError),
        IoError(io::Error),
    }

    DatabaseGetJournalError {
        UnknownJournalError(UnknownJournalError),
    }

    DatabaseGetExtensionError {
        UnknownExtensionError(UnknownExtensionError),
    }

    DatabaseCreateJournalError {
        CatalogAddJournalError(CatalogAddJournalError),
        ConcurrentJournalOpenError(ConcurrentJournalOpenError),
    }

    DatabaseLoadError {
        CatalogAddExtensionError(CatalogAddExtensionError),
        ExtensionResolveError(ExtensionResolveError),
        IoError(io::Error),
    }
);

unit_errors!(
    UnknownJournalError = "Unknown journal",
    UnknownExtensionError = "Unknown extension",
);

/// A database handle.
pub struct Database {
    path: PathBuf,
    catalog: RwLock<Catalog>,
    journals: RwLock<JournalSet>,
    extensions: RwLock<ExtensionSet>,
}

impl Database {
    /// Initialize a database. Give the path to an empty directory.
    pub fn initialize(path: PathBuf) -> Result<(), CatalogInitializeError> {
        let catalog_path = push_path(path.clone(), "CATALOG");
        Catalog::initialize(catalog_path)
    }

    /// Open a database, or fail if it does not exist or seems corrupted.
    pub fn open(path: PathBuf) -> Result<Self, DatabaseOpenError> {
        let catalog_path = push_path(path.clone(), "CATALOG");
        let catalog_scratch_path = push_path(path.clone(), "CATALOG_SCRATCH");
        let catalog = Catalog::open(catalog_path, catalog_scratch_path)?;

        let mut journals = JournalSet::new();
        for (_, &journal_id) in catalog.journals() {
            journals.open_journal(path.clone(), journal_id)?;
        }

        let mut extensions = ExtensionSet::new();
        for (extension_name, extension_path) in catalog.extensions() {
            extensions.load_extension(extension_name.clone(),
                                      extension_path)?;
        }

        let catalog = RwLock::new(catalog);
        let journals = RwLock::new(journals);
        let extensions = RwLock::new(extensions);
        Ok(Database{path, catalog, journals, extensions})
    }

    pub fn get_journal(&self, journal_name: &[u8])
                   -> Result<Arc<ConcurrentJournal>, DatabaseGetJournalError> {
        let journal_id_option = {
            let catalog = self.catalog.read().unwrap();
            catalog.get_journal(journal_name)
        };
        match journal_id_option {
            None => {
                let error = UnknownJournalError;
                let error = DatabaseGetJournalError::UnknownJournalError(error);
                return Err(error);
            },
            Some(journal_id) => {
                let journals = self.journals.read().unwrap();
                let journal = journals.get_journal(journal_id).unwrap();
                Ok(journal)
            },
        }
    }

    pub fn get_extension(&self, extension_name: &[u8])
                     -> Result<Arc<Extension>, DatabaseGetExtensionError> {
        let extension_option =
            self.extensions.read().unwrap()
            .get_extension(extension_name);
        match extension_option {
            None => {
                let error = UnknownExtensionError;
                let error = DatabaseGetExtensionError::UnknownExtensionError(error);
                return Err(error);
            },
            Some(extension) => Ok(extension),
        }
    }
}

impl Database {
    pub fn create_journal(&self, journal_name: RcVec<u8>) -> Result<(), DatabaseCreateJournalError> {
        let journal_id = {
            let mut catalog = self.catalog.write().unwrap();
            catalog.add_journal(journal_name)?
        };
        {
            let mut journals = self.journals.write().unwrap();
            journals.open_journal(self.path.clone(), journal_id)?;
        }
        Ok(())
    }

    pub fn load(&self, name: RcVec<u8>, path: RcVec<u8>) -> Result<(), DatabaseLoadError> {
        let path = PathBuf::from(OsString::from_vec(path));
        {
            let mut catalog = self.catalog.write().unwrap();
            catalog.add_extension(name.clone(), path.clone())?;
        }
        {
            let mut extensions = self.extensions.write().unwrap();
            extensions.load_extension(name, path)?;
        }
        Ok(())
    }
}

/// A set of open journals identified by their identifiers.
pub struct JournalSet {
    journals: HashMap<Uuid, Arc<ConcurrentJournal>>,
}

impl JournalSet {
    /// A new empty set of open journals.
    pub fn new() -> Self {
        let journals = HashMap::new();
        JournalSet{journals}
    }

    /// Open a journal. If it was already open, close and reopen it. If it does
    /// not yet exist, create it.
    pub fn open_journal(&mut self, path: PathBuf, journal_id: Uuid)
                        -> Result<(), ConcurrentJournalOpenError> {
        let checkpoints_filename = format!("{:X}.CHECKPOINTS", journal_id);
        let checkpoints_path = push_path(path.clone(), checkpoints_filename);
        let entries_filename = format!("{:X}.ENTRIES", journal_id);
        let entries_path = push_path(path, entries_filename);
        let journal = ConcurrentJournal::open(checkpoints_path, entries_path)?;
        self.journals.insert(journal_id, Arc::new(journal));
        Ok(())
    }

    /// Get an open journal if it exists.
    pub fn get_journal(&self, journal_id: Uuid) -> Option<Arc<ConcurrentJournal>> {
        self.journals.get(&journal_id).map(|a| a.clone())
    }
}

/// A set of loaded extensions identified by their names.
pub struct ExtensionSet {
    extensions: HashMap<RcVec<u8>, Arc<Extension>>,
}

impl ExtensionSet {
    pub fn new() -> Self {
        let extensions = HashMap::new();
        ExtensionSet{extensions}
    }

    /// Load an extension. If it was already loaded, unload and reload it.
    pub fn load_extension<P>(&mut self, extension_name: RcVec<u8>, path: P)
                             -> Result<(), DatabaseLoadError>
        where P: AsRef<Path> {
        let dl = unsafe { Dl::open(path) }?;
        let extension = Extension::resolve(dl)?;
        self.extensions.insert(extension_name, Arc::new(extension));
        Ok(())
    }

    /// Get a loaded extension if it exists.
    pub fn get_extension(&self, extension_name: &[u8])
                         -> Option<Arc<Extension>> {
        self.extensions.get(extension_name).map(|a| a.clone())
    }
}
