
#[allow(non_camel_case_types)]
pub enum Convention{
    /// sanename-package naming convention
    sanename,
    /// JavaClassName naming convention
    ClassCase,
    /// camelCase with lowercase first word naming convention
    camelCase,
    /// CONSTANT_CASE name convention
    CONSTANT_CASE,
    /// kernel_case, aka snake_case naming conventions
    snake_case
}

/// Validate a sanename package name
pub fn validate(name: &str) -> bool {
	validate_sanename(name)
}

/// Validate a sanename package name format i.e. `[a-z][a-z0-9]*(-[a-z][a-z0-9]*)*`
/// e.g. `sanename-package2`
pub fn validate_sanename(name: &str) -> bool {
    let mut was_dash= false;
    for (i, ch) in name.chars().enumerate() {
        if i == 0 && !ch.is_ascii_lowercase() {
            return false;
        } else if ch == '-' {
            if was_dash {
                return false;
            }
            was_dash = true;
        } else if ! ch.is_ascii_lowercase() && ! ch.is_ascii_digit() {
            return false;
        } else {
            was_dash = false;
        }
    }
    true
}

/// Validate a single word in a sanename, i.e. no dashes for separating words.
/// e.g. `sanename`
pub fn validate_sanename_word(name: &str) -> bool {
    for (i, ch) in name.chars().enumerate() {
        if i == 0 && !ch.is_ascii_lowercase() {
            return false;
        } else if ! ch.is_ascii_lowercase() && ! ch.is_ascii_digit() {
            return false;
        }
    }
    true
}

/// Validate CamelCase: with leading uppercase letter.
/// e.g. `SanenamePackage2`
pub fn validate_class_name(name: &str) -> bool {
    for (i, ch) in name.chars().enumerate() {
        if i == 0 && !ch.is_ascii_uppercase() {
            return false;
        } else if ! ch.is_ascii_alphanumeric() {
            return false;
        }
    }
    true
}

/// Validate camelCase: with lowercase leading letter.
/// e.g. `sanenamePackage2`
pub fn validate_camel_case(name: &str) -> bool {
    for (i, ch) in name.chars().enumerate() {
        if i == 0 && !ch.is_ascii_lowercase() {
            return false;
        } else if ! ch.is_ascii_alphanumeric() {
            return false;
        }
    }
    true
}

/// Validate CONSTANT_CASE: uppercase separated by underscore
/// e.g. `SANENAME_PACKAGE2`
pub fn validate_constant(name: &str) -> bool {
    let mut was_underscore = false;
    for (i, ch) in name.chars().enumerate() {
        if i == 0 && !ch.is_ascii_uppercase() {
            return false;
        } else if ch == '_' {
            if was_underscore {
                return false;
            }
            was_underscore = true;
        } else if ! ch.is_ascii_uppercase()  && ! ch.is_ascii_digit() {
            return false;
        } else {
            was_underscore = false
        }
    }
    true
}

/// Validate kernel_case: lowercase separated by underscore
/// e.g. `sanename_package2`
pub fn validate_snake_case(name: &str) -> bool {
    let mut was_underscore = false;
    for (i, ch) in name.chars().enumerate() {
        if i == 0 && !ch.is_ascii_lowercase() {
            return false;
        } else if ch == '_' {
            if was_underscore {
                return false;
            }
            was_underscore = true;
        } else if ! ch.is_ascii_lowercase() && ! ch.is_ascii_digit() {
            return false;
        } else {
            was_underscore = false
        }
    }
    true
}

/// sanenames should be 3 - 32 chars, validation of this is optional.
pub fn validate_len(name: &str) -> bool {
    let len = name.len();
    len >= 3 && len <= 32
}

/// Convert a package-name format string to another naming convention.
///
/// `name` - assume to already be sanename-package
///
/// returns a String following the specified naming convention, in input is invalid output is undefined.
pub fn convert_to(name: &str, c: Convention) -> String {
    let mut was_dash = false;
    let mut s = String::with_capacity(name.len() + 4);
    match c {
        Convention::camelCase => {
            for ch in name.chars() {
                if ch == '-' || ch == '_' {
                    was_dash = true;
                    continue;
                }
                if was_dash {
                    s.push(ch.to_ascii_uppercase());
                } else {
                    s.push(ch.to_ascii_lowercase());
                }
                was_dash = false;
            }
        },
        Convention::ClassCase => {
            for (i, ch) in name.chars().enumerate() {
                if i == 0 || was_dash {
                    s.push(ch.to_ascii_uppercase());
                    was_dash = false;
                    continue;
                }
                if ch == '-' || ch == '_' {
                    was_dash = true;
                    continue;
                }
                s.push(ch.to_ascii_lowercase());
                was_dash = false;
            }
        },
        Convention::CONSTANT_CASE => {
            for ch in name.chars() {
                if ch == '-' || ch == '_' {
                    s.push('_');
                } else {
                    s.push(ch.to_ascii_uppercase());
                }
            }
        },
        Convention::snake_case => {
            for ch in name.chars() {
                if ch == '-' || ch == '_' {
                    s.push('_');
                } else {
                    s.push(ch);
                }
            }
        },
        Convention::sanename => {
            s = name.to_string();
        }
    }

    s
}

/// Convert a string with another naming convention to sanename-package convention.
///
/// `name` - assumed to already be correctly formatted
pub fn convert_from(name: &str, c: Convention) -> String {
    let mut s = String::with_capacity(name.len() + 4);
    match c {
        Convention::camelCase => {
            for ch in name.chars() {
                if ch.is_ascii_uppercase() {
                    s.push('-');
                }
                s.push(ch.to_ascii_lowercase());
            }
        },
        Convention::ClassCase => {
            for (i, ch) in name.chars().enumerate() {
                if i > 0 && ch.is_ascii_uppercase() {
                    s.push('-');
                }
                s.push(ch.to_ascii_lowercase());
            }
        },
        Convention::CONSTANT_CASE => {
            for ch in name.chars() {
                if ch == '_' {
                    s.push('-');
                }
                else {
                    s.push(ch.to_ascii_lowercase());
                }
            }
        },
        Convention::snake_case => {
            for ch in name.chars() {
                if ch == '_' {
                    s.push('-');
                } else {
                    s.push(ch);
                }
            }
        },
        Convention::sanename => {
            s = name.to_string();
        }
    }

    s
}

/// Takes a String in any format and returns sanename-package format discarding stuff sanename
/// does not approve of.
/// Common delimiters are converted to at most one sequential -
pub fn sanitize(name: &str) -> String {
    let mut s = String::with_capacity(name.len() + 4);
    let mut start = true;
    let mut was_delim = false;
    for ch in name.chars() {
        if start && (ch.is_ascii_digit() || ! ch.is_ascii_alphabetic()) {
            continue;
        }
        start = false;
        if ch.is_ascii_alphanumeric() {
            s.push(ch.to_ascii_lowercase());
            was_delim = false;
        }
        if !was_delim &&
            (
           ch == '-' ||
           ch == '_' ||
           ch == '+' ||
           ch == ' ' ||
           ch == '/' ||
           ch == '.' ||
           ch == '\\' ||
           ch == '|'
            ) {
            s.push('-');
            was_delim = true;
        }
    }
    s = String::from(s.trim_end_matches('-'));

    s
}

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

    #[test]
    fn test_happy_path() {
        assert!(validate("sanename-package1"));
        assert!(validate_sanename_word("sanename"));
        assert!(validate_snake_case("sanename_package1"));
        assert!(validate_camel_case("sanenamePackage1"));
        assert!(validate_class_name("SanenamePackage1"));
        assert!(validate_constant("SANENAME_PACKAGE1"));

        assert_eq!("sanename-package1", convert_to("sanename-package1", Convention::sanename).as_str());
        assert_eq!("sanename_package1", convert_to("sanename-package1", Convention::snake_case).as_str());
        assert_eq!("sanenamePackage1", convert_to("sanename-package1", Convention::camelCase).as_str());
        assert_eq!("SanenamePackage1", convert_to("sanename-package1", Convention::ClassCase).as_str());
        assert_eq!("SANENAME_PACKAGE1", convert_to("sanename-package1", Convention::CONSTANT_CASE).as_str());

        assert_eq!("sanename-package1", convert_from("sanename-package1", Convention::sanename).as_str());
        assert_eq!("sanename-package1", convert_from("sanename_package1", Convention::snake_case).as_str());
        assert_eq!("sanename-package1", convert_from("sanenamePackage1", Convention::camelCase).as_str());
        assert_eq!("sanename-package1", convert_from("SanenamePackage1", Convention::ClassCase).as_str());
        assert_eq!("sanename-package1", convert_from("SANENAME_PACKAGE1", Convention::CONSTANT_CASE).as_str());
    }


    /// single digit words are allowed
    #[test]
    fn test_single_digit_words() {
        assert!(validate("sanename-a-package1"));
        assert!(validate_snake_case("sanename_a_package1"));
        assert!(validate_camel_case("sanenameAPackage1"));
        assert!(validate_class_name("SanenameAPackage1"));
        assert!(validate_constant("SANENAME_A_PACKAGE1"));

        assert_eq!("sanename-a-package1", convert_to("sanename-a-package1", Convention::sanename).as_str());
        assert_eq!("sanename_a_package1", convert_to("sanename-a-package1", Convention::snake_case).as_str());
        assert_eq!("sanenameAPackage1", convert_to("sanename-a-package1", Convention::camelCase).as_str());
        assert_eq!("SanenameAPackage1", convert_to("sanename-a-package1", Convention::ClassCase).as_str());
        assert_eq!("SANENAME_A_PACKAGE1", convert_to("sanename-a-package1", Convention::CONSTANT_CASE).as_str());

        assert_eq!("sanename-a-package1", convert_from("sanename-a-package1", Convention::sanename).as_str());
        assert_eq!("sanename-a-package1", convert_from("sanename_a_package1", Convention::snake_case).as_str());
        assert_eq!("sanename-a-package1", convert_from("sanenameAPackage1", Convention::camelCase).as_str());
        assert_eq!("sanename-a-package1", convert_from("SanenameAPackage1", Convention::ClassCase).as_str());
        assert_eq!("sanename-a-package1", convert_from("SANENAME_A_PACKAGE1", Convention::CONSTANT_CASE).as_str());
    }

    #[test]
    fn test_two_way_conversion() {
        let test = "sanename-a-package1";

        let converted = convert_to(test, Convention::CONSTANT_CASE);
        let back = convert_from(converted.as_str(), Convention::CONSTANT_CASE);
        assert_eq!(test, back);

        let converted = convert_to(test, Convention::ClassCase);
        let back = convert_from(converted.as_str(), Convention::ClassCase);
        assert_eq!(test, back);

        let converted = convert_to(test, Convention::camelCase);
        let back = convert_from(converted.as_str(), Convention::camelCase);
        assert_eq!(test, back);

        let converted = convert_to(test, Convention::snake_case);
        let back = convert_from(converted.as_str(), Convention::snake_case);
        assert_eq!(test, back);
    }

    #[test]
    fn test_validate_negs() {
        // leading digits not allowed
        assert!(!validate("1sanename-package1"));
        assert!(!validate_sanename_word("sanename-wrong"));
        assert!(!validate_snake_case("1sanename_package1"));
        assert!(!validate_camel_case("1sanenamePackage1"));
        assert!(!validate_class_name("1SanenamePackage1"));
        assert!(!validate_constant("1SANENAME_PACKAGE1"));
    }


    #[test]
    fn test_sanitize() {
        assert_eq!("sanename-a-package1", sanitize("-000Sane$naMe----A Pack*age1@$%---"));
    }
}