// Copyright 2021 MaidSafe.net limited.
//
// This SAFE Network Software is licensed to you under the MIT license <LICENSE-MIT
// http://opensource.org/licenses/MIT> or the Modified BSD license <LICENSE-BSD
// https://opensource.org/licenses/BSD-3-Clause>, at your option. This file may not be copied,
// modified, or distributed except according to those terms. Please review the Licences for the
// specific language governing permissions and limitations relating to use of the SAFE Network
// Software.

use crate::{
    api::app::{
        consts::*,
        helpers::{gen_timestamp_secs, systemtime_to_rfc3339},
    },
    Error, Result,
};
use log::debug;
use std::{collections::BTreeMap, fs, path::Path};

#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;

// To use for mapping files names (with path in a flattened hierarchy) to FileItems
pub type FilesMap = BTreeMap<String, FileItem>;

// Each FileItem contains file metadata and the link to the file's Blob XOR-URL
pub type FileItem = BTreeMap<String, String>;

// A trait to get an key attr and return an API Result
pub trait GetAttr {
    fn getattr(&self, key: &str) -> Result<&str>;
}

impl GetAttr for FileItem {
    // Makes it more readable to conditionally get an attribute from a FileItem
    // because we can call it in API funcs like fileitem.getattr("key")?;
    fn getattr(&self, key: &str) -> Result<&str> {
        match self.get(key) {
            Some(v) => Ok(v),
            None => Err(Error::EntryNotFound(format!("key not found: {}", key))),
        }
    }
}

// Represents file metadata.  Simplifies passing it around.
// note: all values are String or Option<String>
// to facilitate use with FileItem.
pub(crate) struct FileMeta {
    created: String,
    modified: String,
    pub(crate) file_size: String,
    pub(crate) file_type: String,
    readonly: Option<String>,
    mode_bits: Option<String>,
    original_created: Option<String>,
    original_modified: Option<String>,
}

impl FileMeta {
    // Instantiates FileMeta from a local filesystem path.
    pub(crate) fn from_path(path: &str, follow_links: bool) -> Result<Self> {
        let (metadata, file_type) = get_metadata(&Path::new(path), follow_links)?;

        // created and modified may not be available on all platforms/filesystems.
        let original_created = if let Ok(time) = metadata.created() {
            Some(systemtime_to_rfc3339(&time))
        } else {
            None
        };
        let original_modified = if let Ok(time) = metadata.modified() {
            Some(systemtime_to_rfc3339(&time))
        } else {
            None
        };
        let readonly = Some(metadata.permissions().readonly().to_string());

        // We use 0 as file_size for metadata such as directories, symlinks.
        let file_size = if metadata.file_type().is_file() {
            metadata.len().to_string()
        } else {
            "0".to_string()
        };

        #[cfg(windows)]
        let mode_bits = None; // Todo:  what does git do for windows?

        #[cfg(not(windows))]
        let mode_bits = Some(metadata.permissions().mode().to_string());

        let s = Self {
            created: gen_timestamp_secs(),
            modified: gen_timestamp_secs(),
            file_size,
            file_type,
            readonly,
            mode_bits,
            original_created,
            original_modified,
        };
        Ok(s)
    }

    // Instantiates FileMeta from a FileItem
    pub(crate) fn from_file_item(file_item: &FileItem) -> Self {
        // The first 4 must be present, else a crash.
        // lots of other code relies on this, so big refactor
        // would be needed to change it.
        let created = file_item[FAKE_RDF_PREDICATE_CREATED].to_string();
        let modified = file_item[FAKE_RDF_PREDICATE_MODIFIED].to_string();
        let file_size = file_item[FAKE_RDF_PREDICATE_SIZE].to_string();
        let file_type = file_item[FAKE_RDF_PREDICATE_TYPE].to_string();

        // These are all Option<String>
        let original_created = file_item
            .get(FAKE_RDF_PREDICATE_ORIGINAL_CREATED)
            .map(ToOwned::to_owned);
        let original_modified = file_item
            .get(FAKE_RDF_PREDICATE_ORIGINAL_MODIFIED)
            .map(ToOwned::to_owned);
        let readonly = file_item
            .get(FAKE_RDF_PREDICATE_READONLY)
            .map(ToOwned::to_owned);
        let mode_bits = file_item
            .get(FAKE_RDF_PREDICATE_MODE_BITS)
            .map(ToOwned::to_owned);

        Self {
            created,
            modified,
            file_size,
            file_type,
            readonly,
            mode_bits,
            original_created,
            original_modified,
        }
    }

    // Instantiates FileMeta from just type and size properties.
    pub(crate) fn from_type_and_size(file_type: &str, file_size: &str) -> Self {
        Self {
            created: gen_timestamp_secs(),
            modified: gen_timestamp_secs(),
            file_size: file_size.to_string(),
            file_type: file_type.to_string(),
            readonly: None,
            mode_bits: None,
            original_created: None,
            original_modified: None,
        }
    }

    // converts Self to FileItem
    pub(crate) fn to_file_item(&self) -> FileItem {
        let mut file_item = FileItem::new();
        Self::add_to_fileitem(
            &mut file_item,
            FAKE_RDF_PREDICATE_CREATED,
            Some(self.created.clone()),
        );
        Self::add_to_fileitem(
            &mut file_item,
            FAKE_RDF_PREDICATE_MODIFIED,
            Some(self.modified.clone()),
        );
        Self::add_to_fileitem(
            &mut file_item,
            FAKE_RDF_PREDICATE_SIZE,
            Some(self.file_size.clone()),
        );
        Self::add_to_fileitem(
            &mut file_item,
            FAKE_RDF_PREDICATE_TYPE,
            Some(self.file_type.clone()),
        );
        Self::add_to_fileitem(
            &mut file_item,
            FAKE_RDF_PREDICATE_READONLY,
            self.readonly.clone(),
        );
        Self::add_to_fileitem(
            &mut file_item,
            FAKE_RDF_PREDICATE_MODE_BITS,
            self.mode_bits.clone(),
        );
        Self::add_to_fileitem(
            &mut file_item,
            FAKE_RDF_PREDICATE_ORIGINAL_CREATED,
            self.original_created.clone(),
        );
        Self::add_to_fileitem(
            &mut file_item,
            FAKE_RDF_PREDICATE_ORIGINAL_MODIFIED,
            self.original_modified.clone(),
        );

        file_item
    }

    // returns false if a directory or symlink, true if anything else (a file).
    pub(crate) fn filetype_is_file(file_type: &str) -> bool {
        !matches!(
            file_type,
            MIMETYPE_FILESYSTEM_DIR | MIMETYPE_FILESYSTEM_SYMLINK
        )
    }

    // returns false if a directory or symlink, true if anything else (a file).
    pub(crate) fn filetype_is_symlink(file_type: &str) -> bool {
        file_type == MIMETYPE_FILESYSTEM_SYMLINK
    }

    // returns false if a directory or symlink, true if anything else (a file).
    pub(crate) fn filetype_is_dir(file_type: &str) -> bool {
        file_type == MIMETYPE_FILESYSTEM_DIR
    }

    // returns false if a directory or symlink, true if anything else (a file).
    pub(crate) fn is_file(&self) -> bool {
        Self::filetype_is_file(&self.file_type)
    }

    pub(crate) fn is_symlink(&self) -> bool {
        Self::filetype_is_symlink(&self.file_type)
    }

    pub(crate) fn is_dir(&self) -> bool {
        Self::filetype_is_dir(&self.file_type)
    }

    // helper: adds property to FileItem if val.is_some()
    fn add_to_fileitem(file_item: &mut FileItem, key: &str, val: Option<String>) {
        if let Some(v) = val {
            file_item.insert(key.to_string(), v);
        }
    }
}

// Get file metadata from local filesystem
pub(crate) fn get_metadata(path: &Path, follow_links: bool) -> Result<(fs::Metadata, String)> {
    let result = if follow_links {
        fs::metadata(path)
    } else {
        fs::symlink_metadata(path)
    };
    let metadata = result.map_err(|err| {
        Error::FileSystemError(format!(
            "Couldn't read metadata from source path ('{}'): {}",
            path.display(),
            err
        ))
    })?;
    debug!("Metadata for location: {:?}", metadata);

    let media_type = get_media_type(path, &metadata);
    Ok((metadata, media_type))
}

fn get_media_type(path: &Path, meta: &fs::Metadata) -> String {
    // see: https://stackoverflow.com/questions/18869772/mime-type-for-a-directory
    // We will use the FreeDesktop standard for directories and symlinks.
    //   https://specifications.freedesktop.org/shared-mime-info-spec/shared-mime-info-spec-latest.html#idm140625828597376
    if meta.file_type().is_dir() {
        return MIMETYPE_FILESYSTEM_DIR.to_string();
    } else if meta.file_type().is_symlink() {
        return MIMETYPE_FILESYSTEM_SYMLINK.to_string();
    }
    let mime_type = mime_guess::from_path(&path);
    let media_type = mime_type.first_raw().unwrap_or("Raw");
    media_type.to_string()
}
