use heck::ToLowerCamelCase;
use std::collections::hash_map::RandomState;
use std::collections::{HashMap, HashSet};
use std::hash::{BuildHasher, Hash};

#[derive(Default)]
pub struct LocalNames {
    // map from exact name to generated local name
    local_name_ids: HashMap<u64, String>,
    local_names: HashSet<String>,
    random_state: RandomState,
}

impl<'a> LocalNames {
    /// provide known global identifier names to exclude from being assigned
    pub fn exclude_globals(&mut self, globals: &[&str]) {
        for name in globals {
            self.local_names.insert(name.to_string());
        }
    }

    /// get a unique identifier name for a string once (can't be looked up again)
    pub fn create_once(&'a mut self, goal_name: &str) -> &'a str {
        let goal_name = if let Some(last_char) = goal_name.rfind('/') {
            &goal_name[last_char + 1..]
        } else {
            goal_name
        };
        let mut goal = to_js_identifier(goal_name);
        if self.local_names.contains(&goal) {
            let mut idx = 1;
            loop {
                let valid_name_suffixed = format!("{goal}${idx}");
                if !self.local_names.contains(&valid_name_suffixed) {
                    goal = valid_name_suffixed;
                    break;
                }
                idx += 1;
            }
        }
        self.local_names.insert(goal.to_string());
        self.local_names.get(&goal).unwrap()
    }

    pub fn get<H: Hash>(&'a self, unique_id: H) -> &'a str {
        let hash = self.random_state.hash_one(&unique_id);
        if !self.local_name_ids.contains_key(&hash) {
            panic!("Internal error, no name defined in local names map");
        }
        &self.local_name_ids[&hash]
    }

    pub fn try_get<H: Hash>(&'a self, unique_id: H) -> Option<&'a str> {
        let hash = self.random_state.hash_one(&unique_id);
        if !self.local_name_ids.contains_key(&hash) {
            return None;
        }
        Some(&self.local_name_ids[&hash])
    }

    /// Get or create a unique identifier for a string while storing the lookup by unique id
    ///
    /// NOTE: we must be careful here to ensure that the object being hashed w/ a similar goal name
    /// are *the same* hashable object -- (ex. `Id<T>` vs `ResourceIndex`)
    pub fn get_or_create<H: Hash>(&'a mut self, unique_id: H, goal_name: &str) -> (&'a str, bool) {
        let hash = self.random_state.hash_one(&unique_id);
        let mut seen = true;
        #[allow(clippy::map_entry)]
        if !self.local_name_ids.contains_key(&hash) {
            let goal = self.create_once(goal_name).to_string();
            self.local_name_ids.insert(hash, goal);
            seen = false;
        }
        (self.local_name_ids.get(&hash).unwrap(), seen)
    }
}

// Convert an arbitrary string to a similar close js identifier
pub fn to_js_identifier(goal_name: &str) -> String {
    if is_valid_js_identifier(goal_name) {
        goal_name.to_string()
    } else {
        let goal = goal_name.to_lower_camel_case();
        let mut identifier = String::new();
        for char in goal.chars() {
            let valid_char = if identifier.is_empty() {
                is_valid_js_identifier_start(char)
            } else {
                is_valid_js_identifier_char(char)
            };
            if valid_char {
                identifier.push(char);
            } else {
                identifier.push(match char {
                    '.' => '_',
                    _ => '$',
                });
            }
        }
        if !is_valid_js_identifier(&identifier) {
            identifier = format!("_{identifier}");
            if !is_valid_js_identifier(&identifier) {
                panic!("Unable to generate valid identifier {identifier} for '{goal_name}'");
            }
        }
        identifier
    }
}

pub fn is_valid_js_identifier(s: &str) -> bool {
    let mut chars = s.chars();
    if let Some(char) = chars.next() {
        if !is_valid_js_identifier_start(char) {
            return false;
        }
    } else {
        return false;
    }
    for char in chars {
        if !is_valid_js_identifier_char(char) {
            return false;
        }
    }
    !is_js_reserved_word(s)
}

pub fn is_js_reserved_word(s: &str) -> bool {
    RESERVED_KEYWORDS.binary_search(&s).is_ok()
}

// https://tc39.es/ecma262/#prod-IdentifierStartChar
// Unicode ID_Start | "$" | "_"
fn is_valid_js_identifier_start(code: char) -> bool {
    match code {
        'A'..='Z' | 'a'..='z' | '$' | '_' => true,
        // leaving out non-ascii for now...
        _ => false,
    }
}

// https://tc39.es/ecma262/#prod-IdentifierPartChar
// Unicode ID_Continue | "$" | U+200C | U+200D
fn is_valid_js_identifier_char(code: char) -> bool {
    match code {
        '0'..='9' | 'A'..='Z' | 'a'..='z' | '$' | '_' => true,
        // leaving out non-ascii for now...
        _ => false,
    }
}

pub fn maybe_quote_id(name: &str) -> String {
    if is_valid_js_identifier(name) {
        name.to_string()
    } else {
        format!("'{name}'")
    }
}

pub fn maybe_quote_member(name: &str) -> String {
    if name == "*" {
        "".to_string()
    } else if is_valid_js_identifier(name) {
        format!(".{name}")
    } else {
        format!("['{name}']")
    }
}

pub(crate) const RESERVED_KEYWORDS: &[&str] = &[
    "await",
    "break",
    "case",
    "catch",
    "class",
    "const",
    "continue",
    "debugger",
    "default",
    "delete",
    "do",
    "eval",
    "else",
    "enum",
    "export",
    "extends",
    "false",
    "finally",
    "for",
    "function",
    "if",
    "implements",
    "import",
    "in",
    "instanceof",
    "interface",
    "let",
    "new",
    "null",
    "package",
    "private",
    "protected",
    "public",
    "return",
    "static",
    "super",
    "switch",
    "this",
    "throw",
    "true",
    "try",
    "typeof",
    "var",
    "void",
    "while",
    "with",
    "yield",
];
