{-# LANGUAGE ParallelListComp #-}

module Language.Bitcoin.Script.Descriptors.Checksum (
  validDescriptorChecksum,
  descriptorChecksum,
) where

import Data.Bifunctor (first)
import Data.Bits (Bits (shiftL, shiftR, testBit, xor, (.&.)))
import Data.Char (ord)
import Data.Foldable (foldl')
import Data.IntMap (IntMap)
import qualified Data.IntMap.Strict as IntMap
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as Text
import Data.Vector.Unboxed (Vector)
import qualified Data.Vector.Unboxed as Vector

{- | Test whether the textual representation of an output descriptor has the
 given checksum.
-}
validDescriptorChecksum :: Text -> Text -> Bool
validDescriptorChecksum desc checksum =
  case mapM (charsetFind checksumCharset) (Text.unpack checksum) of
    Nothing -> False
    Just checkSymbols ->
      1 == polymodChecksum (expandChecksum desc <> checkSymbols)

{- | Compute the checksum of the textual representation of an output descriptor
 if possible.
-}
descriptorChecksum :: Text -> Maybe Text
descriptorChecksum desc = Text.pack <$> sequenceA checksumChars
 where
  checksumChars = [checksumCharset `charsetGet` charsetIndex i | i <- [0 .. 7]]
  charsetIndex i = (checksum `shiftR` (5 * (7 - i))) .&. 31
  symbols = expandChecksum desc <> replicate 8 0
  checksum = 1 `xor` polymodChecksum symbols

expandChecksum :: Text -> [Word]
expandChecksum =
  reverse . end
    . foldl'
      ( \(gs, s) v -> case (v `shiftR` 5 : gs, v .&. 31 : s) of
          ([g2, g1, g0], s') -> ([], 9 * g0 + 3 * g1 + g2 : s')
          x -> x
      )
      mempty
    . fromMaybe []
    . mapM (charsetFind inputCharset)
    . Text.unpack
 where
  end ([g0], s) = g0 : s
  end ([g1, g0], s) = 3 * g0 + g1 : s
  end (_, s) = s

polymodChecksum :: [Word] -> Word
polymodChecksum =
  foldl'
    ( \chk value ->
        foldl'
          xor
          ((chk .&. 0x7ffffffff) `shiftL` 5 `xor` value)
          [if chk `testBit` i then g else 0 | i <- [35 ..] | g <- generator]
    )
    1
 where
  generator =
    [ 0xf5dee51989
    , 0xa9fdca3312
    , 0x1bab10e32d
    , 0x3706b1677a
    , 0x644d626ffd
    ]

data Charset = Charset
  { charToIndex :: IntMap Word
  , indexToChar :: Vector Char
  }

charsetFromString :: String -> Charset
charsetFromString s =
  let xs = [(c, i) | c <- s | i <- [0 ..]]
   in Charset
        { charToIndex = IntMap.fromList $ first ord <$> xs
        , indexToChar = Vector.fromList $ fst <$> xs
        }

charsetFind :: Charset -> Char -> Maybe Word
charsetFind set c = IntMap.lookup (ord c) $ charToIndex set

charsetGet :: Charset -> Word -> Maybe Char
charsetGet set i = indexToChar set Vector.!? fromInteger (toInteger i)

inputCharset :: Charset
inputCharset = charsetFromString "0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ "

checksumCharset :: Charset
checksumCharset = charsetFromString "qpzry9x8gf2tvdw0s3jn54khce6mua7l"
