use crate::attr::Ino;
use crate::consts::*;
use crate::utils::TqlQuote;
use crate::utils::TqlQuotedStr;
use std::borrow::Cow;
use std::char::REPLACEMENT_CHARACTER;
use std::fmt;
use std::fmt::Debug;
use std::fmt::Display;

/// Character used by FS when label reported by Tocco contains a
/// slash ('/'). A slash in not allowed by in a FS.
const SLASH_SUBSTITUTE_CHARACTER: char = '⫽';

/// Character used to start escape sequence.
///
/// Used to encode certain labels that are not allowed
/// in Tocco.
const ESCAPE_CHARACTER: char = REPLACEMENT_CHARACTER;

/// Escaped "." path
const ESCAPED_CURRENT_PATH: &str = "�.";

/// Escaped ".." path
const ESCAPED_PARENT_PATH: &str = "�..";

/// Whether name is valid for a (this) file system
///
/// Valid here means that we never expect this name being
/// reported by by the kernel.
pub fn is_valid_fs_name(name: &str) -> bool {
    !name.contains('/') && name != "." && name != ".." && !name.is_empty()
}

/// Whether name is a valid label in Tocco
///
/// Valid here means that we never expecpt this label being
/// reported by Tocco. Tocco may reject other names when
/// creating a file / folder / domain.
pub fn is_valid_tocco_name(name: &str) -> bool {
    !name.is_empty()
}

/// Encode Tocco label for use in file system
fn encode<'a>(name: &'a str) -> Cow<'a, str> {
    if name == "." {
        Cow::Borrowed(ESCAPED_CURRENT_PATH)
    } else if name == ".." {
        Cow::Borrowed(ESCAPED_PARENT_PATH)
    } else if name.contains(['/', ESCAPE_CHARACTER, SLASH_SUBSTITUTE_CHARACTER]) {
        let mut s = String::new();
        for c in name.chars() {
            match c {
                ESCAPE_CHARACTER => {
                    s.push(ESCAPE_CHARACTER);
                    s.push(ESCAPE_CHARACTER);
                }
                SLASH_SUBSTITUTE_CHARACTER => {
                    s.push(ESCAPE_CHARACTER);
                    s.push(SLASH_SUBSTITUTE_CHARACTER);
                }
                '/' => s.push(SLASH_SUBSTITUTE_CHARACTER),
                _ => s.push(c),
            }
        }
        Cow::Owned(s)
    } else {
        Cow::Borrowed(name)
    }
}

/// Decode filesystem name for use in Tocco
fn decode<'a>(name: &'a str) -> Option<Cow<'a, str>> {
    if name == ESCAPED_CURRENT_PATH {
        Some(Cow::Borrowed("."))
    } else if name == ESCAPED_PARENT_PATH {
        Some(Cow::Borrowed(".."))
    } else if name.contains([ESCAPE_CHARACTER, SLASH_SUBSTITUTE_CHARACTER]) {
        let mut s = String::new();
        let mut iter = name.chars().peekable();
        while let Some(c) = iter.next() {
            match c {
                ESCAPE_CHARACTER => match iter.peek() {
                    Some(&ESCAPE_CHARACTER) => {
                        s.push(ESCAPE_CHARACTER);
                        let _ = iter.next();
                    }
                    Some(&SLASH_SUBSTITUTE_CHARACTER) => {
                        s.push(SLASH_SUBSTITUTE_CHARACTER);
                        let _ = iter.next();
                    }
                    _ => {
                        // Escape sequence invalid.
                        //
                        // This was not encoded by `encode()`.
                        return None;
                    }
                },
                SLASH_SUBSTITUTE_CHARACTER => s.push('/'),
                _ => s.push(c),
            }
        }
        Some(Cow::Owned(s))
    } else {
        Some(Cow::Borrowed(name))
    }
}

/// Representition of a file name
///
/// # Encoding of Invalid FS File Names
///
/// Tocco allows certain file names (labels) which aren't allowed in
/// a file system. Such names are encoded to allow accessing such
/// files via FS anyway:
///
/// * `'/'` is encoded as [`SLASH_SUBSTITUTE_CHARACTER`]
/// * The name `"." is encoded as [`ESCAPE_CHARACTER`] + `"."`.
/// * The name `".." is encoded as [`ESCAPE_CHARACTER`] + `".."`.
/// * `SLASH_SUBSTITUTE_CHARACTER` is encoded as `SLASH_SUBSTITUTE_CHARACTER`
///   + `SLASH_SUBSTITUTE_CHARACTER`.
///
/// The goal is to allow access to all files in Tocco. However,
/// in the other direction, no attempt is made to allow storing
/// arbitrary file names in Tocco.
///
/// # Invisible Files
///
/// Invisible files, files in `".lost+found"`, have the inode info encoded
/// in their file name. For instance, in the name `"f123456-name.text"`,
/// `'f'` indicates that this is a file and `123456` is its key.
///
/// This is done for two reasons, a) to ensure names in .lost+found
/// are unique, and b) to allow retrieving type and key without storing
/// this information in memory.
#[derive(Clone)]
pub struct FileName {
    fs_name: String,
    tocco_name: String,
    /// Inode of orphaned file, as encoded in `tocco_name`.
    ino: Option<Ino>,
}

impl FileName {
    pub fn from_fs_name<'a, T>(parent: Ino, name: T) -> Option<Self>
    where
        T: TryInto<&'a str>,
    {
        if parent.is_lost_and_found() {
            Self::from_orphaned_fs_name(name)
        } else {
            Some(Self::from_regular_fs_name(name)?)
        }
    }

    /// `FileName` from file name of non-orphaned file
    fn from_regular_fs_name<'a, T>(name: T) -> Option<Self>
    where
        T: TryInto<&'a str>,
    {
        let name = name.try_into().ok()?;
        debug_assert!(is_valid_fs_name(name));
        Some(Self {
            fs_name: name.to_owned(),
            tocco_name: decode(name)?.into_owned(),
            ino: None,
        })
    }

    /// `FileName` from file name of an orphaned file
    ///
    /// Returns `None` if file name doesn't encode type and key
    /// (i.e. if it isn't a valid name for an orphaned file).
    fn from_orphaned_fs_name<'a, T>(name: T) -> Option<Self>
    where
        T: TryInto<&'a str>,
    {
        let name = name.try_into().ok()?;
        debug_assert!(is_valid_fs_name(name));
        let (r#type, rest) = name.split_at_checked(1)?;
        let r#type = match r#type {
            "f" => FILE_TYPE,
            "d" => FOLDER_TYPE,
            _ => return None,
        };
        let (key, rest) = rest.split_once('-')?;
        let key: u64 = key.parse().ok()?;
        let ino = Ino::new(key, r#type);

        Some(Self {
            fs_name: name.to_owned(),
            tocco_name: decode(rest)?.to_string(),
            ino: Some(ino),
        })
    }

    /// `FileName` from a Tocco label belonging to a non-orphaned folder
    pub fn from_tocco_name(name: &str) -> Self {
        debug_assert!(is_valid_tocco_name(name));
        Self {
            fs_name: encode(name).into_owned(),
            tocco_name: name.to_owned(),
            ino: None,
        }
    }

    /// `FileName` belonging to a folder in an orphaned folder
    pub fn from_tocco_name_lost_and_found_folder(name: &str, key: u64) -> Self {
        let ino = Ino::new(key, FOLDER_TYPE);
        debug_assert!(is_valid_tocco_name(name));
        Self {
            fs_name: format!("d{key:07}-{}", encode(name)),
            tocco_name: name.to_owned(),
            ino: Some(ino),
        }
    }

    /// `FileName` belonging to a folder in an orphaned file
    pub fn from_tocco_name_lost_and_found_file(name: &str, key: u64) -> Self {
        let ino = Ino::new(key, FILE_TYPE);
        debug_assert!(is_valid_tocco_name(name));
        Self {
            fs_name: format!("f{key:07}-{}", encode(name)),
            tocco_name: name.to_owned(),
            ino: Some(ino),
        }
    }

    /// Get file name encoded for use in FS
    pub fn fs_file_name(&self) -> &str {
        &self.fs_name
    }

    /// Get file name / label as encoded for use in Tocco
    pub fn tocco_file_name(&self) -> &str {
        &self.tocco_name
    }

    /// `Ino` as encoded in file name
    ///
    /// `None` if file / folder isn't orphaned.
    pub fn lost_and_found_entry_ino(&self) -> Option<Ino> {
        self.ino
    }
}

impl Debug for FileName {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        Debug::fmt(&self.fs_name, f)
    }
}

impl TqlQuote for FileName {
    fn tql_quote(&self) -> impl Display {
        TqlQuotedStr(&self.tocco_name)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn en_and_decode() {
        let decoded = "Path with /, ⫽ and � in it.";
        let encoded = encode(decoded);
        assert_eq!(encoded, "Path with ⫽, �⫽ and �� in it.");
        assert_eq!(decoded, decode(&encoded).unwrap());
    }

    #[test]
    fn more_en_and_decode() {
        let test_paths = [
            ".",
            "..",
            "...",
            "�",
            "��",
            "���",
            "/",
            "//",
            "///",
            "⫽",
            "⫽⫽",
            "⫽⫽⫽",
            r"\",
            r"�⫽���⫽",
            "-��//��///��⋱�/�⫽-",
            "test.text",
            "a/b.text",
        ];
        for path in test_paths {
            let encoded = encode(path);
            assert!(is_valid_fs_name(&encoded));
            assert_eq!(path, decode(&encoded).unwrap());
        }
    }

    #[test]
    fn encoded_current_and_parent_path() {
        assert_eq!(decode(ESCAPED_CURRENT_PATH).unwrap(), ".");
        assert_eq!(decode(ESCAPED_PARENT_PATH).unwrap(), "..");
    }
}
