//! Expand identifiers in paths.
//!
//! This struct can be used to expand any path that has BASH-like variables.
//!
//! # Examples
//!
//! ```
//! use komichi::expand::Expand;
//! use camino::Utf8PathBuf;
//!
//! fn fetch(key: &str) -> Option<String> {
//!     match key {
//!         "ONE" => Some("1".to_string()),
//!         "TWO" => Some("two".to_string()),
//!         "THREE" => Some("三".to_string()),
//!         "FOUR" => Some("四".to_string()),
//!         "FIVE" => Some("五".to_string()),
//!         "SIX" => Some("陸".to_string()),
//!         _ => None
//!     }
//! }
//!
//! let mut expand = Expand::new()
//!     .set_home("/a/b/c")
//!     .unwrap()
//!     .set_cwd("/coruscant")
//!     .unwrap()
//!     .path("/")
//!     .unwrap();
//!
//! let paths = vec![
//!     "/$ONE/${TWO}/九十九",
//!     "/${ONE}/$TWO/${THREE}/${SIX}",
//!     "~/${ONE}/$TWO/${THREE}/${FIVE}",
//!     "${FOUR}/$FIVE/${SIX}/七",
//! ];
//!
//! let expects = vec![
//!     Utf8PathBuf::from("/1/two/九十九"),
//!     Utf8PathBuf::from("/1/two/三/陸"),
//!     Utf8PathBuf::from("/a/b/c/1/two/三/五"),
//!     Utf8PathBuf::from("/coruscant/四/五/陸/七"),
//! ];
//!
//! for (i, path) in paths.iter().enumerate() {
//!     expand = expand.path(&path).unwrap();
//!     expand = expand.with(fetch).unwrap();
//!     let result = expand.as_utf8_path_buf();
//!     let expect = expects.get(i).unwrap();
//!     assert_eq!(expect, &result);
//! }
//! ```
//!

use super::error::{ExpandError, ExpandLexerError};

use camino::{Utf8Path, Utf8PathBuf};
use std::marker::PhantomData;
use std::path::MAIN_SEPARATOR_STR;
use std::{collections::VecDeque, convert::AsRef, string::ToString};
use unicode_segmentation::UnicodeSegmentation;

#[derive(Debug, Clone, PartialEq)]
enum Token {
    /// Represents the tilde when used as the first (wide) character and
    /// may be expanded to the users home directory
    IdentifierTilde,

    /// Represents an identifier (variable) to be expanded
    ///
    /// # Attributes
    ///
    /// * `name`- The identifier's name (variable name)
    /// * `uses_curly` - A boolean representing if curly brackets were used
    ///
    Identifier { name: String, uses_curly: bool },

    /// Represents the operating system's path-separator
    Separator,

    /// Represents any component within the path that is not an identifier.
    ///
    /// # Attributes
    ///
    /// `1` - The text
    Text(String),
}

#[derive(Clone, Debug)]
struct Parts<'a> {
    index: usize,
    started: bool,
    wide_chars: Vec<&'a str>,
}

impl<'a> Iterator for Parts<'a> {
    type Item = &'a str;

    #[inline]
    fn next(&mut self) -> Option<&'a str> {
        if self.started {
            self.index += 1;
        } else {
            self.started = true;
        }
        let out = self.nth(self.index);
        if out.is_none() {
            self.index = 0;
            self.started = false;
        }
        out
    }

    #[inline]
    fn nth(&mut self, index: usize) -> Option<&'a str> {
        match self.wide_chars.get(index) {
            Some(s) => Some(*s),
            None => None,
        }
    }
}

impl<'a> Parts<'a> {
    fn new(text: &'a str) -> Self {
        let wide_chars =
            UnicodeSegmentation::graphemes(text, true).collect::<Vec<&str>>();
        Self {
            index: 0usize,
            started: false,
            wide_chars,
        }
    }

    #[inline]
    fn peek(&mut self) -> Option<&'a str> {
        let mut index = self.index;
        if self.started {
            index += 1;
        }
        self.nth(index)
    }

    #[inline]
    fn bump_index(&mut self) {
        self.index += 1;
    }

    #[inline]
    fn possible_token_count(&self) -> usize {
        let mut count = 0usize;
        for item in self.wide_chars.iter() {
            if item == &MAIN_SEPARATOR_STR {
                count += 1
            }
        }
        count = count * 2 + 1;
        count
    }

    fn extract_text(&mut self) -> String {
        let mut out = String::new();
        let mut escape = false;
        while let Some(wide_char) = self.peek() {
            if !escape {
                match wide_char {
                    "\\" => {
                        escape = true;
                        out.push_str(wide_char);
                        self.bump_index();
                        continue;
                    },
                    "$" => break,
                    MAIN_SEPARATOR_STR => break,
                    _ => {},
                }
            }
            out.push_str(wide_char);
            escape = false;
            self.bump_index();
        }
        out
    }

    fn extract_identifier(
        &mut self,
    ) -> Result<(String, bool), ExpandLexerError> {
        let mut start_position = 0usize;
        let mut curly = false;
        let mut uses_curly = false;
        let mut curly_start_position = 0usize;
        let mut started = false;
        let mut out = String::new();
        let mut position = 0usize;
        while let Some(wide_char) = self.peek() {
            if position == 0 {
                if wide_char != "$" {
                    return Err(ExpandLexerError::MissingIdentifierSymbol(
                        self.index + 1,
                    ));
                }
                position += 1;
                start_position = self.index + 1;
                self.bump_index();
                continue;
            }
            if position == 1 && wide_char == "{" {
                curly = true;
                uses_curly = true;
                curly_start_position = self.index + 1;
                position += 1;
                self.bump_index();
                continue;
            }
            if !wide_char.is_ascii() {
                break;
            }
            if !started {
                if is_identifier_start(wide_char) {
                    out.push_str(wide_char);
                    self.bump_index();
                    position += 1;
                    started = true;
                    continue;
                }
                self.bump_index();
                if uses_curly && wide_char == "}" {
                    return Err(ExpandLexerError::MissingIdentifierName(
                        self.index - 1,
                    ));
                }
                return Err(
                    ExpandLexerError::invalid_identifier_start_character(
                        wide_char,
                        self.index - 1,
                    ),
                );
            }
            if is_identifier(wide_char) {
                out.push_str(wide_char);
                self.bump_index();
                position += 1;
                continue;
            }
            if curly && wide_char == "}" {
                self.bump_index();
                curly = false;
            }
            break;
        }
        if curly {
            return Err(ExpandLexerError::MissingClosingCurly(
                curly_start_position,
            ));
        }
        if out.is_empty() {
            return Err(ExpandLexerError::MissingIdentifierName(
                start_position,
            ));
        }
        Ok((out, uses_curly))
    }
}

#[inline(always)]
fn is_identifier_start(wide_char: &str) -> bool {
    if !wide_char.is_ascii() {
        return false;
    }
    let c = wide_char.chars().next().unwrap_or_default();
    if c.is_alphabetic() {
        return true;
    }
    if c == '_' {
        return true;
    }
    false
}

#[inline(always)]
fn is_identifier(wide_char: &str) -> bool {
    if !wide_char.is_ascii() {
        return false;
    }
    let c = wide_char.chars().next().unwrap_or_default();
    if c.is_alphanumeric() {
        return true;
    }
    if c == '_' {
        return true;
    }
    false
}

fn tokenize<T>(text: &T) -> Result<VecDeque<Token>, ExpandLexerError>
where
    T: AsRef<Utf8Path> + ?Sized,
{
    let text = text.as_ref().to_string();
    let mut parts = Parts::new(text.as_ref());

    let mut out: VecDeque<Token> =
        VecDeque::with_capacity(parts.possible_token_count());

    let mut position = 0usize;
    let mut started = false;
    while let Some(wide_char) = parts.peek() {
        if started {
            position += 1;
        } else {
            started = true;
        }
        if position == 0 && wide_char == "~" {
            if let Some(s) = parts.nth(1usize) {
                if s != MAIN_SEPARATOR_STR {
                    return Err(ExpandLexerError::InvalidTildeUse(
                        MAIN_SEPARATOR_STR.to_string(),
                    ));
                }
            }
            parts.bump_index();
            out.push_back(Token::IdentifierTilde);
            continue;
        }
        match wide_char {
            "\\" => {
                out.push_back(Token::Text(parts.extract_text()));
                continue;
            },
            MAIN_SEPARATOR_STR => {
                parts.bump_index();
                out.push_back(Token::Separator);
                position += 1;
                continue;
            },
            "$" => {
                let (name, uses_curly) = parts.extract_identifier()?;
                out.push_back(Token::Identifier { name, uses_curly });
            },
            _ => out.push_back(Token::Text(parts.extract_text())),
        }
    }
    Ok(out)
}

#[inline(always)]
fn tokens_to_string(tokens: &[Token]) -> String {
    let mut out = String::new();
    for token in tokens.iter() {
        match token {
            Token::IdentifierTilde => out.push('~'),
            Token::Separator => out.push_str(MAIN_SEPARATOR_STR),
            Token::Text(val) => out.push_str(val),
            Token::Identifier { name, uses_curly } => {
                out.push('$');
                if *uses_curly {
                    out.push('{');
                }
                out.push_str(name);
                if *uses_curly {
                    out.push('}');
                }
            },
        }
    }
    out
}

/// Represents a locked [`Expand`] struct.
pub struct Locked;
///
/// Represents an unlocked [`Expand`] struct.
pub struct Unlocked;

/// A struct used for expanding file-system path strings.
pub struct Expand<State = Locked> {
    home: Option<Utf8PathBuf>,
    cwd: Option<Utf8PathBuf>,
    fetch: Option<fn(&str) -> Option<String>>,
    state: PhantomData<State>,
}

impl Default for Expand {
    fn default() -> Self {
        Self::new()
    }
}

impl Expand {
    /// Return a new [`Expand`] struct.
    pub fn new() -> Expand<Locked> {
        let home = Utf8PathBuf::new();
        let cwd = Utf8PathBuf::new();
        let path = String::new();
        Expand {
            home: None,
            cwd: None,
            fetch: None,
            state: Default::default(),
        }
    }
}

impl Expand<Unlocked> {
    fn path_as_tokens(
        &self,
        path: &str,
    ) -> Result<VecDeque<Token>, ExpandError> {
        tokenize(&path).map_err(|e| ExpandError::lexer_error(&path, e))
    }

    /// Replace a starting tilde ~ with the home directory.
    ///
    /// # Errors
    /// * [`ExpandError`] will be returned if:
    ///   * `home` has not been set (set with [`Self::set_home`]); or,
    ///   * the path is contains an open curly bracket `{` with no preceeding
    ///     dollar sign `$`; or,
    ///   * the identifier has an invalid start character; or,
    ///   * the identifier is missing a closing curly bracket `}`; or,
    ///   * the identifier name is missing; or,
    ///   * path invalid character after a starting tilde `~`; or
    ///
    fn expand_tilde(&self, tokens: &mut VecDeque<Token>) {
        if let Some(token) = tokens.get(0) {
            match token {
                Token::IdentifierTilde => {
                    let _ = tokens.pop_front();
                },
                _ => return,
            }
        }
        let home = match self.home {
            Some(path) => path,
            None => return,
        };
        let new_token = Token::Text(home.to_string());
        tokens.push_front(new_token);
    }
    /// Prepend a non absolute path with the current working directory ("CWD").
    ///
    /// # Errors
    /// * [`ExpandError`] will be returned if:
    ///   * `cwd` has not been set (set with [`Self::set_cwd`]); or,
    ///   * the path is contains an open curly bracket `{` with no preceeding
    ///     dollar sign `$`; or,
    ///   * the identifier has an invalid start character; or,
    ///   * the identifier is missing a closing curly bracket `}`; or,
    ///   * the identifier name is missing; or,
    ///   * path contains invalid character after a starting tilde `~`; or
    ///
    pub fn expand_cwd(&self, tokens: &mut VecDeque<Token>) {
        if let Some(token) = tokens.get(0) {
            match token {
                Token::Separator => return,
                _ => {},
            }
        }
        let cwd = match self.cwd {
            Some(path) => path,
            None => return,
        };
        let new_token = Token::Text(cwd.to_string());
        tokens.push_front(new_token);
    }

    /// Replace any identifiers, in the path, with the given callback.
    ///
    /// Any identifiers that have no values, provided by the given callback
    /// will be skipped.
    ///
    /// # Arguments
    /// * `fetch` - the function used to provide a value for a given identifier.
    ///
    /// # Errors
    /// * [`ExpandError`] will be returned if:
    ///   * the path is contains an open curly bracket `{` with no preceeding
    ///     dollar sign `$`; or,
    ///   * the identifier has an invalid start character; or,
    ///   * the identifier is missing a closing curly bracket `}`; or,
    ///   * the identifier name is missing; or,
    ///   * path contains invalid character after a starting tilde `~`; or
    ///
    pub fn expand_with(&self, tokens: &mut VecDeque<Token>) {
        let fetch = match self.fetch {
            Some(func) => func,
            None => return,
        };
        for (i, token) in tokens.iter().enumerate() {
            if let Token::Identifier { name, .. } = token {
                if let Some(val) = fetch(name) {
                    tokens[i] = Token::Text(val)
                }
            }
        }
    }

    /// Replace any identifiers, in the path, with the given callback.
    ///
    /// An [`ExpandError`] will be returned on any identifiers that have no
    /// values, provided by the given callback.
    ///
    /// # Arguments
    /// * `fetch` - the function used to provide a value for a given identifier.
    ///
    /// # Errors
    /// * [`ExpandError`] will be returned if:
    ///   * there is no value for a particular identifier
    ///   * the path is contains an open curly bracket `{` with no preceeding
    ///     dollar sign `$`; or,
    ///   * the identifier has an invalid start character; or,
    ///   * the identifier is missing a closing curly bracket `}`; or,
    ///   * the identifier name is missing; or,
    ///   * path contains invalid character after a starting tilde `~`; or
    ///
    pub fn expand_strict_with(
        &self,
        tokens: &mut VecDeque<Token>,
        path: &str,
    ) -> Result<(), ExpandError> {
        let fetch = match self.fetch {
            Some(func) => func,
            None => return Ok(()),
        };
        for (i, token) in tokens.iter().enumerate() {
            if let Token::Identifier { name, .. } = token {
                match fetch(name) {
                    Some(val) => tokens[i] = Token::Text(val),
                    None => {
                        return Err(ExpandError::identifier_expand_error(
                            &path, &name,
                        ));
                    },
                }
            }
        }
        Ok(())
    }
    /// Set the path-to-be-expanded value.
    ///
    /// # Arguments
    /// * `path` - a string-like reference to a [`str`] containing the
    ///   path that will be expanded.
    ///
    /// # Errors
    /// * [`ExpandError`] will be returned if the given path is empty.
    ///
    pub fn path<T>(&self, path: &T) -> Result<Utf8PathBuf, ExpandError>
    where
        T: AsRef<str> + ?Sized,
    {
        let path = path.as_ref().to_string();
        if path.is_empty() {
            return Err(ExpandError::EmptyPathError);
        }
        let mut tokens = self.path_as_tokens(&path)?;

        self.expand_tilde(&tokens);
        self.expand_cwd(&tokens);
        self.expand_with(&tokens);

        let path = tokens_to_string(&tokens);
        Ok(Utf8PathBuf::from(path))
    }

    pub fn path_strict<T>(&self, path: &T) -> Result<Utf8PathBuf, ExpandError>
    where
        T: AsRef<str> + ?Sized,
    {
        let path = path.as_ref().to_string();
        if path.is_empty() {
            return Err(ExpandError::EmptyPathError);
        }
        let mut tokens = self.path_as_tokens(&path)?;

        self.expand_tilde(&tokens);
        self.expand_cwd(&tokens);
        self.expand_with(&tokens);

        let path = tokens_to_string(&tokens);
        Ok(Utf8PathBuf::from(path))
    }
}

impl<State> Expand<State> {
    /// Set the home value to be used when expanding tildes
    ///
    /// # Arguments
    /// * `path` - a string-like reference to a [`str`] containing the
    ///   desired path to the home directory.
    ///
    /// # Errors
    /// * [`ExpandError`] will be returned if the given path is not an
    ///   absolute path.
    ///
    pub fn set_home<T>(self, path: &T) -> Result<Expand<Unlocked>, ExpandError>
    where
        T: AsRef<str> + ?Sized,
    {
        let path = path.as_ref();
        let home = Utf8PathBuf::from(&path);
        if !home.is_absolute() {
            return Err(ExpandError::NotAbsoluteHome(path.to_string()));
        }
        Ok(Expand {
            home: Some(home),
            cwd: self.cwd,
            fetch: self.fetch,
            state: PhantomData::<Unlocked>,
        })
    }
    /// Set the current working directory ("CWD") value to be used when
    /// expanding non absolute paths.
    ///
    /// # Arguments
    /// * `path` - a string-like reference to a [`str`] containing the
    ///   desired path to the CWD.
    ///
    /// # Errors
    /// * [`ExpandError`] will be returned if the given path is not an
    ///   absolute path.
    ///
    pub fn set_cwd<T>(self, path: &T) -> Result<Expand<Unlocked>, ExpandError>
    where
        T: AsRef<str> + ?Sized,
    {
        let path = path.as_ref();
        let cwd = Utf8PathBuf::from(&path);
        if !cwd.is_absolute() {
            return Err(ExpandError::NotAbsoluteCwd(path.to_string()));
        }
        Ok(Expand {
            home: self.home,
            cwd: Some(cwd),
            fetch: self.fetch,
            state: PhantomData::<Unlocked>,
        })
    }
    pub fn set_fetch<F>(&self, fetch: F) -> Expand<Unlocked>
    where
        F: Fn(&str) -> Option<String>,
    {
        Expand {
            home: self.home,
            cwd: self.cwd,
            fetch: Some(fetch),
            state: PhantomData::<Unlocked>,
        }
    }
}
