{-# LANGUAGE
    OverloadedStrings
  , GeneralizedNewtypeDeriving
  , DataKinds
  , ApplicativeDo
#-}

module UUID
  ( encodeHyphenated
  , builderHyphenated
  , decodeHyphenated
  , parserHyphenated
  , encodeUnhyphenated
  , builderUnhyphenated
  , parserUnhyphenated
  , decodeUnhyphenated
  , decodeLenient
  ) where

import Arithmetic.Nat
import Data.Bits
import Data.Bytes.Builder.Bounded
import Data.Bytes
import Data.Bytes.Parser
import Data.Bytes.Parser.Latin
import Data.Primitive.ByteArray
import Data.WideWord.Word128
import Data.Word

-- | In its canonical textual representation, 
-- the 16 octets of a UUID are represented as 32 hexadecimal (base-16) digits, 
-- displayed in 5 groups separated by hyphens,
-- in the form 8-4-4-4-12 for a total of 36 characters 
-- (32 alphanumeric characters and 4 hyphens)
--
-- UUIDs can also be represented as a base62 encoding of a Word128

encodeHyphenated :: Word128 -> ByteArray
encodeHyphenated uuid = run constant (builderHyphenated uuid)

builderHyphenated :: Word128 -> Builder 36
builderHyphenated uuid =
           word32PaddedLowerHex w1
  `append` ascii '-'
  `append` word16PaddedLowerHex w2
  `append` ascii '-'
  `append` word16PaddedLowerHex w3
  `append` ascii '-'
  `append` word16PaddedLowerHex w4
  `append` ascii '-'
  `append` word32PaddedLowerHex w5
  `append` word16PaddedLowerHex w6
  where
  (w1,w2,w3,w4,w5,w6) = toWords uuid

decodeHyphenated :: Bytes -> Maybe Word128
decodeHyphenated uuid = parseBytesMaybe (parserHyphenated ()) uuid

-- | Parser type from @bytesmith@
parserHyphenated :: e -> Parser e s Word128
parserHyphenated err = do
  w1 <- hexFixedWord32 err
  skipChar1 err '-'
  w2 <- hexFixedWord16 err
  skipChar1 err '-'
  w3 <- hexFixedWord16 err
  skipChar1 err '-'
  w4 <- hexFixedWord16 err
  skipChar1 err '-'
  w5 <- hexFixedWord32 err
  w6 <- hexFixedWord16 err
  pure $ Word128
    { word128Hi64 = fromW32W16Word64 w1 w2 w3
    , word128Lo64 = fromW16W32W16Word64 w4 w5 w6
    }

toWords :: Word128 -> (Word32,Word16,Word16,Word16,Word32,Word16)
toWords (Word128 a b) =
  -- Note: implementing this as 2 Word64 shifts with 'unsafeShiftR'
  -- is up to 40% faster than using 128-bit shifts on a Word128 value.
  ( fromIntegral (unsafeShiftR a 32)
  , fromIntegral (unsafeShiftR a 16)
  , fromIntegral a
  , fromIntegral (unsafeShiftR b 48)
  , fromIntegral (unsafeShiftR b 16)
  , fromIntegral b
  )

parserUnhyphenated :: e -> Parser e s Word128
parserUnhyphenated err =
      Word128
  <$> hexFixedWord64 err
  <*> hexFixedWord64 err

decodeUnhyphenated :: Bytes -> Maybe Word128
decodeUnhyphenated uuid = parseBytesMaybe (parserUnhyphenated ()) uuid

builderUnhyphenated :: Word128 -> Builder 32
builderUnhyphenated = word128PaddedLowerHex

encodeUnhyphenated :: Word128 -> ByteArray
encodeUnhyphenated uuid = run constant (builderUnhyphenated uuid)

parserLenient :: e -> Parser e s Word128
parserLenient err = do
  w1 <- hexFixedWord32 err
  skipChar '-'
  w2 <- hexFixedWord16 err
  skipChar '-'
  w3 <- hexFixedWord16 err
  skipChar '-'
  w4 <- hexFixedWord16 err
  skipChar '-'
  w5 <- hexFixedWord32 err
  w6 <- hexFixedWord16 err
  pure $ Word128
    { word128Hi64 = fromW32W16Word64 w1 w2 w3
    , word128Lo64 = fromW16W32W16Word64 w4 w5 w6
    }

-- | decodes uuid with out without hyphens
decodeLenient :: Bytes -> Maybe Word128
decodeLenient uuid = parseBytesMaybe (parserLenient ()) uuid

fromW32W16Word64 :: Word32 -> Word16 -> Word16 -> Word64
fromW32W16Word64 a b c =
      shiftL (fromIntegral a) 32
  .|. shiftL (fromIntegral b) 16
  .|. (fromIntegral c)

fromW16W32W16Word64 :: Word16 -> Word32 -> Word16 -> Word64
fromW16W32W16Word64 a b c =
      shiftL (fromIntegral a) 48
  .|. shiftL (fromIntegral b) 16
  .|. (fromIntegral c)