{-# LANGUAGE CPP              #-}
{-# LANGUAGE TypeApplications #-}

-- | = WARNING
--
-- This module is considered __internal__.
--
-- The Package Versioning Policy __does not apply__.
--
-- The contents of this module may change __in any way whatsoever__
-- and __without any warning__ between minor versions of this package.
--
-- Authors importing this module are expected to track development
-- closely.
--
-- = Description
--
-- Debugging utilities for 'HashMap's.

module Data.HashMap.Internal.Debug
    ( valid
    , Validity(..)
    , Error(..)
    , SubHash
    , SubHashPath
    ) where

import Data.Bits             (complement, countTrailingZeros, popCount, shiftL,
                              unsafeShiftL, (.&.), (.|.))
import Data.Hashable         (Hashable)
import Data.HashMap.Internal (Bitmap, Hash, HashMap (..), Leaf (..),
                              bitsPerSubkey, fullBitmap, hash,
                              isLeafOrCollision, maxChildren, sparseIndex)
import Data.Semigroup        (Sum (..))

import qualified Data.HashMap.Internal.Array as A


#if !MIN_VERSION_base(4,11,0)
import Data.Semigroup (Semigroup (..))
#endif

data Validity k = Invalid (Error k) SubHashPath | Valid
  deriving (Eq, Show)

instance Semigroup (Validity k) where
  Valid <> y = y
  x     <> _ = x

instance Monoid (Validity k) where
  mempty = Valid
  mappend = (<>)

-- | An error corresponding to a broken invariant.
--
-- See 'HashMap' for the documentation of the invariants.
data Error k
  = INV1_internal_Empty
  | INV2_Bitmap_unexpected_1_bits !Bitmap
  | INV3_bad_BitmapIndexed_size !Int
  | INV4_bitmap_array_size_mismatch !Bitmap !Int
  | INV5_BitmapIndexed_invalid_single_subtree
  | INV6_misplaced_hash !Hash
  | INV7_key_hash_mismatch k !Hash
  | INV8_bad_Full_size !Int
  | INV9_Collision_size !Int
  | INV10_Collision_duplicate_key k !Hash
  deriving (Eq, Show)

-- TODO: Name this 'Index'?!
-- (https://github.com/haskell-unordered-containers/unordered-containers/issues/425)
-- | A part of a 'Hash' with 'bitsPerSubkey' bits.
type SubHash = Word

data SubHashPath = SubHashPath
  { partialHash :: !Word
    -- ^ The bits we already know, starting from the lower bits.
    -- The unknown upper bits are @0@.
  , lengthInBits :: !Int
    -- ^ The number of bits known.
  } deriving (Eq, Show)

initialSubHashPath :: SubHashPath
initialSubHashPath = SubHashPath 0 0

addSubHash :: SubHashPath -> SubHash -> SubHashPath
addSubHash (SubHashPath ph l) sh =
  SubHashPath (ph .|. (sh `unsafeShiftL` l)) (l + bitsPerSubkey)

hashMatchesSubHashPath :: SubHashPath -> Hash -> Bool
hashMatchesSubHashPath (SubHashPath ph l) h = maskToLength h l == ph
  where
    -- Note: This needs to use `shiftL` instead of `unsafeShiftL` because
    -- @l'@ may be greater than 32/64 at the deepest level.
    maskToLength h' l' = h' .&. complement (complement 0 `shiftL` l')

valid :: Hashable k => HashMap k v -> Validity k
valid Empty = Valid
valid t     = validInternal initialSubHashPath t
  where
    validInternal p Empty                 = Invalid INV1_internal_Empty p
    validInternal p (Leaf h l)            = validHash p h <> validLeaf p h l
    validInternal p (Collision h ary)     = validHash p h <> validCollision p h ary
    validInternal p (BitmapIndexed b ary) = validBitmapIndexed p b ary
    validInternal p (Full ary)            = validFull p ary

    validHash p h | hashMatchesSubHashPath p h = Valid
                  | otherwise                  = Invalid (INV6_misplaced_hash h) p

    validLeaf p h (L k _) | hash k == h = Valid
                          | otherwise   = Invalid (INV7_key_hash_mismatch k h) p

    validCollision p h ary = validCollisionSize <> A.foldMap (validLeaf p h) ary <> distinctKeys
      where
        n = A.length ary
        validCollisionSize | n < 2     = Invalid (INV9_Collision_size n) p
                           | otherwise = Valid
        distinctKeys = A.foldMap (\(L k _) -> appearsOnce k) ary
        appearsOnce k | A.foldMap (\(L k' _) -> if k' == k then Sum @Int 1 else Sum 0) ary == 1 = Valid
                      | otherwise = Invalid (INV10_Collision_duplicate_key k h) p

    validBitmapIndexed p b ary = validBitmap <> validArraySize <> validSubTrees p b ary
      where
        validBitmap | b .&. complement fullBitmap == 0 = Valid
                    | otherwise                        = Invalid (INV2_Bitmap_unexpected_1_bits b) p
        n = A.length ary
        validArraySize | n < 1 || n >= maxChildren = Invalid (INV3_bad_BitmapIndexed_size n) p
                       | popCount b == n           = Valid
                       | otherwise                 = Invalid (INV4_bitmap_array_size_mismatch b n) p

    validSubTrees p b ary
      | A.length ary == 1
      , isLeafOrCollision (A.index ary 0)
      = Invalid INV5_BitmapIndexed_invalid_single_subtree p
      | otherwise = go b
      where
        go 0  = Valid
        go b' = validInternal (addSubHash p (fromIntegral c)) (A.index ary i) <> go b''
          where
            c = countTrailingZeros b'
            m = 1 `unsafeShiftL` c
            i = sparseIndex b m
            b'' = b' .&. complement m

    validFull p ary = validArraySize <> validSubTrees p fullBitmap ary
      where
        n = A.length ary
        validArraySize | n == maxChildren = Valid
                       | otherwise        = Invalid (INV8_bad_Full_size n) p
