{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedStrings #-}

module Foundation.UUID
    ( UUID(..)
    , newUUID
    , nil
    , fromBinary
    , uuidParser
    ) where

import Data.Maybe (fromMaybe)

import           Basement.Compat.Base
import           Foundation.Collection (Element, Sequential, foldl')
import           Foundation.Class.Storable
import           Foundation.Hashing.Hashable
import           Foundation.Bits
import           Foundation.Parser
import           Foundation.Numerical
import           Foundation.Primitive
import           Basement.Base16
import           Basement.IntegralConv
import           Basement.Types.OffsetSize
import qualified Basement.UArray as UA
import           Foundation.Random (MonadRandom, getRandomBytes)

data UUID = UUID {-# UNPACK #-} !Word64 {-# UNPACK #-} !Word64
    deriving (Eq,Ord,Typeable)
instance Show UUID where
    show = toLString
instance NormalForm UUID where
    toNormalForm !_ = ()
instance Hashable UUID where
    hashMix (UUID a b) = hashMix a . hashMix b
instance Storable UUID where
    peek p = UUID <$> (fromBE <$> peekOff ptr 0)
                  <*> (fromBE <$> peekOff ptr 1)
      where ptr = castPtr p :: Ptr (BE Word64)
    poke p (UUID a b) = do
        pokeOff ptr 0 (toBE a)
        pokeOff ptr 1 (toBE b)
      where ptr = castPtr p :: Ptr (BE Word64)
instance StorableFixed UUID where
    size      _ = 16
    alignment _ = 8

withComponent :: UUID -> (Word32 -> Word16 -> Word16 -> Word16 -> Word64 -> a) -> a
withComponent (UUID a b) f = f x1 x2 x3 x4 x5
  where
    !x1 = integralDownsize (a .>>. 32)
    !x2 = integralDownsize ((a .>>. 16) .&. 0xffff)
    !x3 = integralDownsize (a .&. 0xffff)
    !x4 = integralDownsize (b .>>. 48)
    !x5 = (b .&. 0x0000ffffffffffff)
{-# INLINE withComponent #-}

toLString :: UUID -> [Char]
toLString uuid = withComponent uuid $ \x1 x2 x3 x4 x5 ->
    hexWord_4 x1 $ addDash $ hexWord_2 x2 $ addDash $ hexWord_2 x3 $ addDash $ hexWord_2 x4 $ addDash $ hexWord64_6 x5 []
  where
    addDash = (:) '-'
    hexWord_2 w l = case hexWord16 w of
                         (c1,c2,c3,c4) -> c1:c2:c3:c4:l
    hexWord_4 w l = case hexWord32 w of
                    (c1,c2,c3,c4,c5,c6,c7,c8) -> c1:c2:c3:c4:c5:c6:c7:c8:l
    hexWord64_6 w l = case word64ToWord32s w of
                        Word32x2 wHigh wLow -> hexWord_2 (integralDownsize wHigh) $ hexWord_4 wLow l

nil :: UUID
nil = UUID 0 0

newUUID :: MonadRandom randomly => randomly UUID
newUUID = fromMaybe (error "Foundation.UUID.newUUID: the impossible happned")
        . fromBinary
        <$> getRandomBytes 16

fromBinary :: UA.UArray Word8 -> Maybe UUID
fromBinary ba
    | UA.length ba /= 16 = Nothing
    | otherwise          = Just $ UUID w0 w1
  where
    w0 = (b15 .<<. 56) .|. (b14 .<<. 48) .|. (b13 .<<. 40) .|. (b12 .<<. 32) .|.
         (b11 .<<. 24) .|. (b10 .<<. 16) .|. (b9 .<<. 8)   .|. b8
    w1 = (b7 .<<. 56) .|. (b6 .<<. 48) .|. (b5 .<<. 40) .|. (b4 .<<. 32) .|.
         (b3 .<<. 24) .|. (b2 .<<. 16) .|. (b1 .<<. 8)  .|. b0

    b0  = integralUpsize (UA.unsafeIndex ba 0)
    b1  = integralUpsize (UA.unsafeIndex ba 1)
    b2  = integralUpsize (UA.unsafeIndex ba 2)
    b3  = integralUpsize (UA.unsafeIndex ba 3)
    b4  = integralUpsize (UA.unsafeIndex ba 4)
    b5  = integralUpsize (UA.unsafeIndex ba 5)
    b6  = integralUpsize (UA.unsafeIndex ba 6)
    b7  = integralUpsize (UA.unsafeIndex ba 7)
    b8  = integralUpsize (UA.unsafeIndex ba 8)
    b9  = integralUpsize (UA.unsafeIndex ba 9)
    b10 = integralUpsize (UA.unsafeIndex ba 10)
    b11 = integralUpsize (UA.unsafeIndex ba 11)
    b12 = integralUpsize (UA.unsafeIndex ba 12)
    b13 = integralUpsize (UA.unsafeIndex ba 13)
    b14 = integralUpsize (UA.unsafeIndex ba 14)
    b15 = integralUpsize (UA.unsafeIndex ba 15)

uuidParser :: ( ParserSource input, Element input ~ Char
              , Sequential (Chunk input), Element input ~ Element (Chunk input)
              )
           => Parser input UUID
uuidParser = do
    hex1 <- parseHex (CountOf 8) <* element '-'
    hex2 <- parseHex (CountOf 4) <* element '-'
    hex3 <- parseHex (CountOf 4) <* element '-'
    hex4 <- parseHex (CountOf 4) <* element '-'
    hex5 <- parseHex (CountOf 12)
    return $ UUID (hex1 .<<. 32 .|. hex2 .<<. 16 .|. hex3)
                  (hex4 .<<. 48 .|. hex5)


parseHex :: ( ParserSource input, Element input ~ Char
            , Sequential (Chunk input), Element input ~ Element (Chunk input)
            )
         => CountOf Char -> Parser input Word64
parseHex count = do
    r <- toList <$> take count
    unless (and $ isValidHexa <$> r) $
        reportError $ Satisfy $ Just $ "expecting hexadecimal character only: "
                                    <> fromList (show r)
    return $ listToHex 0 r
  where
    listToHex = foldl' (\acc' x -> acc' * 16 + fromHex x)
    isValidHexa :: Char -> Bool
    isValidHexa c = ('0' <= c && c <= '9') || ('a' <= c && c <= 'f') || ('A' <= c && c <= 'F')
    fromHex '0' = 0
    fromHex '1' = 1
    fromHex '2' = 2
    fromHex '3' = 3
    fromHex '4' = 4
    fromHex '5' = 5
    fromHex '6' = 6
    fromHex '7' = 7
    fromHex '8' = 8
    fromHex '9' = 9
    fromHex 'a' = 10
    fromHex 'b' = 11
    fromHex 'c' = 12
    fromHex 'd' = 13
    fromHex 'e' = 14
    fromHex 'f' = 15
    fromHex 'A' = 10
    fromHex 'B' = 11
    fromHex 'C' = 12
    fromHex 'D' = 13
    fromHex 'E' = 14
    fromHex 'F' = 15
    fromHex _   = error "Foundation.UUID.parseUUID: the impossible happened"