{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DerivingStrategies #-}
module Voting.Protocol.Credential where

import Control.DeepSeq (NFData)
import Control.Monad (Monad(..), forM_, replicateM)
import Data.Bool
import Data.Char (Char)
import Data.Either (Either(..), either)
import Data.Eq (Eq(..))
import Data.Function (($))
import Data.Functor ((<$>))
import Data.Int (Int)
import Data.Maybe (maybe)
import Data.Ord (Ord(..))
import Data.Reflection (Reifies(..))
import Data.Semigroup (Semigroup(..))
import Data.Text (Text)
import GHC.Generics (Generic)
import Prelude (Integral(..), fromIntegral)
import Text.Show (Show(..))
import qualified Control.Monad.Trans.State.Strict as S
import qualified Data.Aeson as JSON
import qualified Data.Aeson.Types as JSON
import qualified Data.Char as Char
import qualified Data.List as List
import qualified Data.Text as Text
import qualified System.Random as Random

import Voting.Protocol.Arithmetic
import Voting.Protocol.Cryptography

-- * Class 'Key'
class Key crypto where
        -- | Type of cryptography, eg. "FFC".
        cryptoType :: crypto -> Text
        -- | Name of the cryptographic paramaters, eg. "Belenios".
        cryptoName :: crypto -> Text
        -- | Generate a random 'SecretKey'.
        randomSecretKey ::
         Reifies c crypto =>
         Monad m => Random.RandomGen r =>
         S.StateT r m (SecretKey crypto c)
        -- | @('credentialSecretKey' uuid cred)@ returns the 'SecretKey'
        -- derived from given 'uuid' and 'cred'
        -- using 'Crypto.fastPBKDF2_SHA256'.
        credentialSecretKey ::
         Reifies c crypto =>
         UUID -> Credential -> SecretKey crypto c
        -- | @('publicKey' secKey)@ returns the 'PublicKey'
        -- derived from given 'SecretKey' @secKey@.
        publicKey ::
         Reifies c crypto =>
         SecretKey crypto c ->
         PublicKey crypto c

-- * Type 'Credential'
-- | A 'Credential' is a word of @('tokenLength'+1 '==' 15)@-characters
-- from a base alphabet of (@'tokenBase' '==' 58)@ characters:
-- "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
-- (beware the absence of "0", \"O", \"I", and "l").
-- The last character is a checksum.
-- The entropy is: @('tokenLength' * log 'tokenBase' / log 2) '==' 82.01… bits@.
newtype Credential = Credential Text
 deriving (Eq,Show,Generic)
 deriving newtype NFData
 deriving newtype JSON.ToJSON
instance JSON.FromJSON Credential where
        parseJSON json@(JSON.String s) =
                either (\err -> JSON.typeMismatch ("Credential: "<>show err) json) return $
                readCredential s
        parseJSON json = JSON.typeMismatch "Credential" json

credentialAlphabet :: [Char] -- TODO: make this an array
credentialAlphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz"
tokenBase :: Int
tokenBase = List.length credentialAlphabet
tokenLength ::Int
tokenLength = 14

-- | @'randomCredential'@ generates a random 'Credential'.
randomCredential :: Monad m => Random.RandomGen r => S.StateT r m Credential
randomCredential = do
        rs <- replicateM tokenLength (randomR (fromIntegral tokenBase))
        let (tot, cs) = List.foldl' (\(acc,ds) d ->
                        ( acc * tokenBase + d
                        , charOfDigit d : ds )
                 ) (zero::Int, []) rs
        let checksum = (negate tot + 53) `mod` 53 -- NOTE: why 53 and not 'tokenBase' ?
        return $ Credential $ Text.reverse $ Text.pack (charOfDigit checksum:cs)
        where
        charOfDigit = (credentialAlphabet List.!!)

-- | @'readCredential'@ reads and check the well-formedness of a 'Credential'
-- from raw 'Text'.
readCredential :: Text -> Either ErrorToken Credential
readCredential s
 | Text.length s /= tokenLength + 1 = Left ErrorToken_Length
 | otherwise = do
        tot <- Text.foldl'
         (\acc c -> acc >>= \a -> ((a * tokenBase) +) <$> digitOfChar c)
         (Right (zero::Int))
         (Text.init s)
        checksum <- digitOfChar (Text.last s)
        if (tot + checksum) `mod` 53 == 0
        then Right (Credential s)
        else Left ErrorToken_Checksum
        where
        digitOfChar c =
                maybe (Left $ ErrorToken_BadChar c) Right $
                List.elemIndex c credentialAlphabet

-- ** Type 'ErrorToken'
data ErrorToken
 =   ErrorToken_BadChar Char.Char
 |   ErrorToken_Checksum
 |   ErrorToken_Length
 deriving (Eq,Show,Generic,NFData)

-- ** Type 'UUID'
newtype UUID = UUID Text
 deriving (Eq,Ord,Show,Generic)
 deriving anyclass (JSON.ToJSON)
 deriving newtype NFData
instance JSON.FromJSON UUID where
        parseJSON json@(JSON.String s) =
                either (\err -> JSON.typeMismatch ("UUID: "<>show err) json) return $
                readUUID s
        parseJSON json = JSON.typeMismatch "UUID" json

-- | @'randomUUID'@ generates a random 'UUID'.
randomUUID ::
 Monad m =>
 Random.RandomGen r =>
 S.StateT r m UUID
randomUUID = do
        rs <- replicateM tokenLength (randomR (fromIntegral tokenBase))
        return $ UUID $ Text.pack $ charOfDigit <$> rs
        where
        charOfDigit = (credentialAlphabet List.!!)

-- | @'readCredential'@ reads and check the well-formedness of a 'Credential'
-- from raw 'Text'.
readUUID :: Text -> Either ErrorToken UUID
readUUID s
 | Text.length s /= tokenLength = Left ErrorToken_Length
 | otherwise = do
        forM_ (Text.unpack s) digitOfChar
        return (UUID s)
        where
        digitOfChar c =
                maybe (Left $ ErrorToken_BadChar c) Right $
                List.elemIndex c credentialAlphabet