{-|
Module      : Name Generator
Description : A library for generating names.
License     : GPL-3
Maintainer  : pommicket@gmail.com
-}

module NameGenerator
(Trigrams,
getTrigrams,
first2Chars,
nextChar,
generateName)
where

import qualified Data.Map as Map
import Data.List (isPrefixOf)
import Data.Char (toUpper)
import Text.Read (readMaybe)
import System.Random (randomRIO)

-- | A type that represents all the frequencies of trigrams.
type Trigrams = Map.Map String Int

-- | Extract a trigram from a line
addTrigram :: Trigrams -- ^ Currrent trigrams
           -> String   -- ^ Line
           -> Trigrams -- ^ New trigrams
addTrigram trigrams line = case (readMaybe value :: Maybe Int) of
                                Just val -> Map.insert trigram val trigrams
                                Nothing  -> error "There was a problem reading trigrams.txt"
                         where trigram = take 3 line
                               value   = drop 4 line

-- | Get all trigrams from trigrams.txt
getTrigrams :: IO Trigrams
getTrigrams = do
                trigrams <- readFile "trigrams.txt"
                return $ foldl addTrigram Map.empty $ lines trigrams

-- | Filter out all trigrams that start with a string
filterStartsWith :: String   -- ^ Only include trigrams that start with...
                 -> Trigrams -- ^ Trigrams
                 -> Trigrams -- ^ New trigrams
filterStartsWith start trigrams = Map.fromList $ filter (\(k, _)->start `isPrefixOf` k) $ Map.toList trigrams

-- | Pick a trigram by the sum of all previous ones.
pick :: Int    -- ^ Which trigram
     -> (String, Int) -- ^ The accumulator. fst: \"\" if it hasn't been found, the trigram otherwise. snd: sum so far
     -> (String, Int) -- ^ (key, value)
     -> (String, Int)  -- ^ fst: The trigram. snd: sum so far

pick n ("", sumSoFar) (k, v)
                           | sum' >= n = (k, 0)
                           | otherwise = ("", sum')
                           where sum' = sumSoFar + v
pick _ x _ = x

-- | Pick a (weighted) random trigram from a list.
pickFromTrigrams :: Trigrams -- ^ Which trigrams to pick from
                 -> IO String -- ^ The trigram
pickFromTrigrams trigrams = do
                                let total = sum $ Map.elems trigrams
                                chosen <- randomRIO (0, total)
                                let (trigram, _) = foldl (pick chosen) ("", 0) $ Map.toList trigrams
                                return trigram

-- | Get the first 2 characters of a name.
first2Chars :: Trigrams        -- ^ List of trigrams
            -> IO String       -- ^ First 2 characters
first2Chars trigrams = do
                        let options = filterStartsWith " " trigrams
                        trigram <- pickFromTrigrams options
                        return $ drop 1 trigram

-- | Take off end of list
takeEnd :: Int -- ^ Number of items
        -> [a] -- ^ List
        -> [a] -- ^ Items

takeEnd n l = drop (length l - n) l

-- | Next character in a name.
nextChar :: Trigrams -- ^ List of trigrams
         -> String   -- ^ Name so far
         -> IO Char  -- ^ Next character

nextChar trigrams name = do
                            let options = filterStartsWith (takeEnd 2 name) trigrams
                            trigram <- pickFromTrigrams options
                            return $ last trigram

generateName' :: Trigrams -- ^ List of trigrams
              -> IO String -- ^ Name so far
              -> IO String -- ^ New name

generateName' trigrams currentName = do
    name <- currentName
    next <- nextChar trigrams name
    if next == ' ' then currentName else generateName' trigrams $ fmap (++[next]) currentName

-- | Capitalizes a string
capitalize :: String -- ^ the string
           -> String -- ^ The capitalized string
capitalize (first:rest) = (toUpper first):rest

-- | Generate a name.
generateName :: Trigrams  -- ^ The list of trigrams
             -> IO String -- ^ The name

generateName trigrams = fmap capitalize $ generateName' trigrams $ first2Chars trigrams