-- | Gray code is a binary numeral system where two successive numbers
-- differ in only one bit.
--
-- This module provides an interface to encode/decode @'Bits'@ types.
--
-- Algorithm:
--   Haupt, R.L. and Haupt, S.E., Practical Genetic Algorithms,
--   Second ed. (2004),  5.4. Gray Codes.
module Codec.Binary.Gray.Bits
    ( gray
    , binary
    , showBits
    ) where

import Data.Bits
    ( Bits, testBit, setBit, clearBit, finiteBitSize, bitSizeMaybe
    , shiftL, shiftR, complement, xor, (.&.), (.|.), isSigned)

import qualified Codec.Binary.Gray.List as L

-- | Right shift without extension of the sign bit (reset it to zero).
--
-- Results on negative values of unbounded integral types (like 'Integer') may be wrong.
shiftR' :: (Bits a, Num a) => a -> Int -> a
shiftR' n s =
  case (bitSizeMaybe n, signum n == (-1)) of
    (Just sz, True) ->
        let n' = clearBit (shiftR n 1) (sz - 1)
        in  shiftR' n' (s-1)
    (_, _) ->
        shiftR n s


-- | Convert an integer number from binary to Gray code.
--
-- Results on negative values of unbounded integral types (like 'Integer') may be wrong.
gray :: (Bits a, Num a) => a -> a
gray n = n `xor` (shiftR' n 1)

-- | Convert an integer number from Gray code to binary.
--
-- Results on negative values of unbounded integral types (like 'Integer') may be wrong.
binary :: (Bits a, Num a) => a -> a
binary 0 = 0
binary n =
    case maybeSz of
      (Just sz) ->
          let lastbit = sz - 1
              mask0 = let m = setBit 0 lastbit in (m, m)
              copyMSB n = (setBit 0 lastbit) .&. n
          in  binary' lastbit mask0 n (copyMSB n)
      Nothing ->  -- unbounded and negative
          0
  where
    maybeSz = case bitSizeMaybe n of
                (Just bsz) -> Just bsz
                Nothing -> effectiveBitSize n


effectiveBitSize :: (Bits a, Num a) => a -> Maybe Int
effectiveBitSize n
    | signum n == (-1) = bitSizeMaybe n
    | otherwise        = Just $ ebs n 0
  where
    ebs n bsz
        | signum n /= 1 = bsz
        | otherwise     = ebs (n `shiftR` 1) (bsz + 1)


binary' lastbit (maskReady, maskLast) ngray nbin
  | (maskReady .&. 1) /= 0 = nbin
  | otherwise =
     let
       nReady = maskReady .&. nbin
       maskReady' = setBit (shiftR maskReady 1) lastbit
       maskLast' = shiftR' maskLast 1
       nNext = (shiftR' (maskLast .&. nReady) 1) `xor` (maskLast' .&. ngray)
     in
       binary' lastbit (maskReady', maskLast') ngray (nReady .|. nNext)

-- | Render binary code as a string of @0@s and @1@s.
-- For example, @(42::Int8)@ is formatted as @101010@.
showBits :: (Bits a, Num a) => a -> String
showBits = L.showBits . L.toList