{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections   #-}

{-|

Copyright:
  This file is part of the package zxcvbn-hs. It is subject to the
  license terms in the LICENSE file found in the top-level directory
  of this distribution and at:

    https://code.devalot.com/sthenauth/zxcvbn-hs

  No part of this package, including this file, may be copied,
  modified, propagated, or distributed except according to the terms
  contained in the LICENSE file.

License: MIT

-}
module Text.Password.Strength.Internal.L33t (
  -- * L33t Speak Substitution
  L33t,
  l33t,
  l33tText,
  l33tSub,
  l33tUnsub,
  l33t2Eng
  ) where

--------------------------------------------------------------------------------
-- Library Imports:
import Control.Lens ((^.))
import Control.Lens.TH (makeLenses)
import Data.Text (Text)
import qualified Data.Text as Text

--------------------------------------------------------------------------------
-- Project Imports:
import Text.Password.Strength.Internal.Token

--------------------------------------------------------------------------------
-- | Track a translated l33t speak token.
data L33t = L33t
  { _l33tText :: Text
    -- ^ The translated (un-l33ted) text.

  , _l33tSub :: Int
    -- ^ Number of substituted l33t characters.

  , _l33tUnsub :: Int
    -- ^ Number of characters in the token that were not substituted
    -- but could have been.

  } deriving Show

makeLenses ''L33t

--------------------------------------------------------------------------------
-- | Translate a token from l33t, counting l33t characters.
l33t :: Token -> [L33t]
l33t token
  | not (Text.any ((>0) . l33tCount) chars) = []
  | otherwise = filter hasSubs (map count trans)
  where
    hasSubs :: L33t -> Bool
    hasSubs L33t{..} = _l33tSub > 0 || _l33tUnsub > 0

    chars :: Text
    chars = (token ^. tokenLower)

    trans :: [(Token, Text)]
    trans  = case translateMap l33t2Eng chars of
               [x] | x == chars -> []
                   | otherwise  -> [(token, x)]
               xs               -> map (token,) xs

    count :: (Token, Text) -> L33t
    count (tk, text) =
      let cnt (x, y) c = (x + l33tCount c, y + engCount c)
          (s, u) = Text.foldl cnt (0, 0) (tk ^. tokenChars)
      in L33t text s u

--------------------------------------------------------------------------------
-- | Convert l33t characters to their English character mappings.
l33t2Eng :: Char -> String
l33t2Eng c =
  case c of
    '!' -> ['i']
    '$' -> ['s']
    '%' -> ['x']
    '(' -> ['c']
    '+' -> ['t']
    '0' -> ['o']
    '1' -> ['i', 'l']
    '2' -> ['z']
    '3' -> ['e']
    '4' -> ['a']
    '5' -> ['s']
    '6' -> ['g']
    '7' -> ['l', 't']
    '8' -> ['b']
    '9' -> ['g']
    '<' -> ['c']
    '@' -> ['a']
    '[' -> ['c']
    '{' -> ['c']
    '|' -> ['i', 'l']
    _   -> []

--------------------------------------------------------------------------------
-- | Character count for English letters that can be converted to l33t.
--
-- There's a theoretically possible under estimation here since some
-- English characters can be converted into multiple l33t characters.
-- In reality, English characters tend to have one primary l33t
-- character associated with them.
--
-- An attacker may try all l33t variations for a given English
-- character, but chances are that the primary character was used so
-- we will estimate guesses based on that assumption.
engCount :: Char -> Int
engCount c =
  case c of
    'a' -> 1
    'b' -> 1
    'c' -> 1
    'e' -> 1
    'g' -> 1
    'i' -> 1
    'l' -> 1
    'o' -> 1
    's' -> 1
    't' -> 1
    'x' -> 1
    'z' -> 1
    _   -> 0

--------------------------------------------------------------------------------
-- | Character count for l33t characters that can be converted to l33t.
l33tCount :: Char -> Int
l33tCount c =
  case c of
    '!' -> 1
    '$' -> 1
    '%' -> 1
    '(' -> 1
    '+' -> 1
    '0' -> 1
    '1' -> 1
    '2' -> 1
    '3' -> 1
    '4' -> 1
    '5' -> 1
    '6' -> 1
    '7' -> 1
    '8' -> 1
    '9' -> 1
    '<' -> 1
    '@' -> 1
    '[' -> 1
    '{' -> 1
    '|' -> 1
    _   -> 0