// Phonograph - A friendly PostgreSQL wrapper for nerds of all stripes.
// Copyright (C) 2025  Second System Technologies LLC
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or (at your
// option) any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
// FOR A PARTICULAR PURPOSE.
//
// See the GNU Affero General Public License for more details. You should have
// received a copy of the GNU Affero General Public License along with this
// program.  If not, see <http://www.gnu.org/licenses/>.

//! A very simple utility crate for generating randomized, memorable names from
//! the EFF passphrase wordlist.
//!
//! ## Examples
//!
//! ### Basic Usage
//!
//! ```
//! let name: String = phono_namegen::default_generator().generate_name(3);
//! ```

use std::sync::LazyLock;

use rand::{Rng, rngs::ThreadRng, seq::SliceRandom};

// EFF wordlist for random password phrases.
//
// > We took all words between 3 and 9 characters from the list, prioritizing
// > the most recognized words and then the most concrete words. We manually
// > checked and attempted to remove as many profane, insulting, sensitive, or
// > emotionally-charged words as possible, and also filtered based on several
// > public lists of vulgar English words (for example this one published by
// > Luis von Ahn). We further removed words which are difficult to spell as
// > well as homophones (which might be confused during recall). We also ensured
// > that no word is an exact prefix of any other word.
//
// [Deep Dive: EFF's New Wordlists for Random Passphrases](
// https://www.eff.org/deeplinks/2016/07/new-wordlists-random-passphrases
// )
//
// Note that the wordlist file should have the dice values removed, such that
// each line contains only the word. This is to simplify data processing as well
// as to help shrink the raw string data packaged with the compiled binary.
const WORDLIST_LEN: usize = 7776;
// TODO: Rewrite this as a compile time macro generating a const instead of a
// LazyLock.
static WORDLIST: LazyLock<[&str; WORDLIST_LEN]> = LazyLock::new(|| {
    let mut words = [""; WORDLIST_LEN];
    // Embed wordlist file rather than attempt to fetch it from disk at runtime.
    for (i, word) in include_str!("./eff_large_wordlist.txt")
        .split('\n')
        .enumerate()
    {
        // The wordlist may contain a trailing newline.
        assert!(i <= WORDLIST_LEN);
        if i < WORDLIST_LEN {
            words[i] = word;
        }
    }

    // Assert that wordlist fills the entire array--in other words, that
    // `WORDLIST_LEN` is accurate.
    assert!(!words[WORDLIST_LEN - 1].is_empty());

    words
});

/// Constructs a [`NameGenerator`] configured to use [`rand::ThreadRng`].
pub fn default_generator() -> NameGenerator<ThreadRng> {
    NameGenerator::<ThreadRng>::default()
}

/// Name generator client.
pub struct NameGenerator<T: Rng> {
    rng: T,
    sep: char,
}

impl<T: Default + Rng> Default for NameGenerator<T> {
    fn default() -> Self {
        Self {
            rng: T::default(),
            sep: '_',
        }
    }
}

impl<T: Rng> NameGenerator<T> {
    /// Set the separator between words in generated names (`'_'` by default).
    pub fn with_separator(mut self, sep: char) -> Self {
        self.sep = sep;
        self
    }

    /// Return `n` randomly selected words from the wordlist, without
    /// repetition.
    pub fn choose_words(&mut self, n: usize) -> Vec<&'static str> {
        WORDLIST
            .choose_multiple(&mut self.rng, n)
            .copied()
            .collect()
    }

    /// Generate a randomized name with `n` words, joined into a single
    /// [`String`].
    pub fn generate_name(&mut self, n: usize) -> String {
        // Temporarily store the UTF8 representation of the [`char`] on the
        // stack to avoid a heap allocation for the conversion to [`&str`].
        let mut sep_buf: [u8; 4] = [0; 4];

        self.choose_words(n)
            .join(self.sep.encode_utf8(&mut sep_buf))
    }
}
