-- | Implements Base 64-encoded VLQ for 32-bit
-- integers. Implementation copied from
-- https://code.google.com/p/closure-compiler/source/browse/trunk/src/com/google/debugging/sourcemap/Base64VLQ.java
--

{-# OPTIONS -Wall #-}

module VLQ
  (encode
  ,decode)
  where

import           Data.Bits hiding (shift)
import           Data.ByteString.Lazy (ByteString)
import qualified Data.ByteString.Lazy as B
import           Data.Int
import           Data.List
import           Data.Word
import           Prelude hiding ((>>))

-- | A Base64 VLQ digit can represent 5 bits, so it is base-32.
baseShift :: Int
baseShift = 5

-- | Base point.
base :: Int32
base = 1 << baseShift

-- | A mask of bits for a VLQ digit (11111), 31 decimal.
baseMask :: Int32
baseMask = base - 1

-- | The continuation bit is the 6th bit.
continuationBit :: Int32
continuationBit = base

-- | Converts from a two-complement value to a value where the sign
-- bit is is placed in the least significant bit.  For example, as
-- decimals:
--   1 becomes 2 (10 binary), -1 becomes 3 (11 binary)
--   2 becomes 4 (100 binary), -2 becomes 5 (101 binary)
toVlqSigned :: Int32 -> Int32
toVlqSigned value =
  if value < 0
     then ((-value) << 1) + 1
     else (value << 1) + 0

-- | Converts to a two-complement value from a value where the sign
-- bit is is placed in the least significant bit.  For example, as
-- decimals:
--   2 (10 binary) becomes 1, 3 (11 binary) becomes -1
--   4 (100 binary) becomes 2, 5 (101 binary) becomes -2
fromVlgSigned :: Int32 -> Int32
fromVlgSigned value =
  let value' = value >> 1
  in if (value & 1) == 1
        then -value'
        else value'

-- | Produces a ByteString containing a VLQ-encoded value of the given 32-bit integer.
encode :: Int32 -> ByteString
encode = B.map encodeBase64 . start where
  start 0 = B.singleton (fst (continue 0))
  start n = B.unfoldr go . toVlqSigned $ n

  go value
    | value <= 0 = Nothing
    | otherwise  = Just (continue value)

  continue value =
    let digit = value & baseMask
        value' = value >> baseShift
        digit' = if value' > 0
                    then digit .|. continuationBit
                    else digit
    in (fromIntegral digit',value')


-- | Decodes the given VLQ-encoded value into a 32-bit integer.
decode :: ByteString -> Int32
decode = fromVlgSigned . go (0,0) . B.map decodeBase64 where
  go (result,shift) bytes =
    case B.uncons bytes of
      Nothing -> result
      Just (c,next) ->
        let digit = fromIntegral c
            continuation = (digit & continuationBit) /= 0
            digit' = digit & baseMask
            result' = result + (digit' << shift)
            shift' = shift + baseShift
        in if continuation
              then go (result',shift') next
              else result'

-- | Base 64 characters.
base64Chars :: [Word8]
base64Chars = map (fromIntegral.fromEnum) "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"

-- | Encode the given number to a base 64 character.
encodeBase64 :: Word8 -> Word8
encodeBase64 i = maybe (error "Base 64 char must be between 0 and 63.")
                       id
                       (lookup i (zip [0..] base64Chars))

-- | Encode the given base 64 character to a number.
decodeBase64 :: Word8 -> Word8
decodeBase64 i = maybe (error "Not a valid base 65 digit.")
                       id
                        (lookup i (zip base64Chars [0..]))

-- | Makes the code more familiar to read. Shift-left.
(<<) :: Int32 -> Int -> Int32
(<<) = shiftL

-- | Makes the code more familiar to read. Shift-right.
(>>) :: Int32 -> Int -> Int32
(>>) = shiftR

-- | Makes the code more familiar to read. And.
(&) :: Int32 -> Int32 -> Int32
(&) = (.&.)