{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE HexFloatLiterals #-}
{-# LANGUAGE NumericUnderscores #-}
{-# LANGUAGE RankNTypes #-}
module RoundingSpec where
import           Control.Monad
import           Data.Int
import           Data.Proxy
import           Data.Ratio
import           Data.Word
import           Numeric
import           Numeric.Floating.IEEE
import           Numeric.Floating.IEEE.Internal
import           Test.Hspec
import           Test.Hspec.QuickCheck
import           Test.QuickCheck hiding (classify)
import           Util

newtype RoundTiesTowardZero a = RoundTiesTowardZero { roundTiesTowardZero :: a }
  deriving (Functor)

instance RoundingStrategy RoundTiesTowardZero where
  exact = RoundTiesTowardZero
  inexact o _neg _parity zero away = RoundTiesTowardZero $ case o of
                                                             LT -> zero
                                                             EQ -> zero
                                                             GT -> away
  doRound _exact o _neg _parity zero away = RoundTiesTowardZero $ case o of
    LT -> zero
    EQ -> zero
    GT -> away

newtype RoundToOdd a = RoundToOdd { roundToOdd :: a }
  deriving (Functor)

instance RoundingStrategy RoundToOdd where
  exact = RoundToOdd
  inexact _o _neg parity zero away | even parity = RoundToOdd away
                                   | otherwise = RoundToOdd zero
  doRound exact _o _neg parity zero away | not exact && even parity = RoundToOdd away
                                         | otherwise = RoundToOdd zero

newtype Exactness a = Exactness { isExact :: Bool }
  deriving (Functor)

instance RoundingStrategy Exactness where
  exact _ = Exactness True
  inexact _o _neg _parity _zero _away = Exactness False
  doRound exact _o _neg _parity _zero _away = Exactness exact

prop_fromIntegerR_vs_fromIntegralR :: (RealFloat a, RoundingStrategy f, Integral i) => Proxy a -> Proxy i -> (f a -> a) -> i -> Property
prop_fromIntegerR_vs_fromIntegralR _ _ f m =
  let x = f (fromIntegerR (toInteger m))
      y = f (fromIntegralR m)
  in x `sameFloatP` y

prop_fromIntegerR_vs_fromRationalR :: (RealFloat a, RoundingStrategy f) => Proxy a -> (f a -> a) -> Integer -> Property
prop_fromIntegerR_vs_fromRationalR _ f m =
  let x = f (fromIntegerR m)
      y = f (fromRationalR (m % 1))
  in x `sameFloatP` y

prop_fromIntegerR_vs_encodeFloatR :: (RealFloat a, RoundingStrategy f) => Proxy a -> (f a -> a) -> Integer -> NonNegative Int -> Property
prop_fromIntegerR_vs_encodeFloatR _ f m (NonNegative k) =
  let x = f (fromIntegerR m)
      y = f (encodeFloatR (m * floatRadix x ^ k) (-k))
  in x `sameFloatP` y

prop_fromRationalR_vs_encodeFloatR :: (RealFloat a, RoundingStrategy f) => Proxy a -> (f a -> a) -> Integer -> Int -> Property
prop_fromRationalR_vs_encodeFloatR _ f m k =
  let x = f (fromRationalR (fromInteger m * fromInteger (floatRadix x) ^^ k))
      y = f (encodeFloatR m k)
  in x `sameFloatP` y

prop_fromRationalR_vs_fromRational :: RealFloat a => Proxy a -> Rational -> Property
prop_fromRationalR_vs_fromRational proxy q =
  let x = roundTiesToEven (fromRationalR q) `asProxyTypeOf` proxy
      y = fromRational q `asProxyTypeOf` proxy
  in x `sameFloatP` y

prop_scaleFloatR_vs_fromRationalR :: (RealFloat a, RoundingStrategy f) => Proxy a -> (f a -> a) -> Int -> a -> Property
prop_scaleFloatR_vs_fromRationalR proxy f e x = isFinite x && not (isNegativeZero x) ==>
  let base = floatRadix x
      y = f (scaleFloatR e x)
      z = f (fromRationalR (toRational x * fromInteger base^^e))
  in y `sameFloatP` z

prop_scaleFloatR_vs_encodeFloatR :: (RealFloat a, RoundingStrategy f) => Proxy a -> (f a -> a) -> Int -> a -> Property
prop_scaleFloatR_vs_encodeFloatR proxy f e x = isFinite x && not (isNegativeZero x) ==>
  let base = floatRadix x
      (m,n) = decodeFloat x
      y = f (scaleFloatR e x)
      z = f (encodeFloatR m (n + e))
  in y `sameFloatP` z

prop_encodeFloatR_roundtrip :: (RealFloat a, RoundingStrategy f) => Proxy a -> a -> (f a -> a) -> Property
prop_encodeFloatR_roundtrip proxy x rounding = isFinite x && not (isNegativeZero x) ==>
  let (m,n) = decodeFloat x
  in rounding (encodeFloatR m n) `sameFloatP` x

prop_order :: RealFloat a => Proxy a -> (forall f. RoundingStrategy f => f a) -> Property
prop_order _ result =
  let tiesToEven = roundTiesToEven result
      tiesToAway = roundTiesToAway result
      tiesTowardZero = roundTiesTowardZero result
      up = roundTowardPositive result
      down = roundTowardNegative result
      zero = roundTowardZero result
      toOdd = roundToOdd result
  in if isExact result then
       counterexample "exact case" $ conjoin
       [ counterexample "tiesToAway == tiesToEven" $ tiesToAway `sameFloatP` tiesToEven
       , counterexample "tiesTowardZero == tiesToEven" $ tiesTowardZero `sameFloatP` tiesToEven
       , counterexample "upward == tiesToEven" $ up `sameFloatP` tiesToEven
       , counterexample "downward == tiesToEven" $ down `sameFloatP` tiesToEven
       , counterexample "towardZero == tiesToEven" $ zero `sameFloatP` tiesToEven
       , counterexample "toOdd == tiesToEven" $ toOdd `sameFloatP` tiesToEven
       ]
     else
       counterexample "inexact case" $ conjoin
       [ counterexample "down < up" $ down < up
       , counterexample "down <= tiesToEven" $ down <= tiesToEven
       , counterexample "down <= tiesToAway" $ down <= tiesToAway
       , counterexample "down <= tiesTowardZero" $ down <= tiesTowardZero
       , counterexample "down <= towardZero" $ down <= zero
       , counterexample "down <= odd" $ down <= toOdd
       , counterexample "tiesToEven <= up" $ tiesToEven <= up
       , counterexample "tiesToAway <= up" $ tiesToAway <= up
       , counterexample "tiesTowardZero <= up" $ tiesTowardZero <= up
       , counterexample "towardZero <= up" $ zero <= up
       , counterexample "odd <= up" $ toOdd <= up
       , counterexample "nextUp down == up" $ nextUp down `sameFloatP` up
       , counterexample "down == nextDown up" $ down `sameFloatP` nextDown up
       , counterexample "abs towardZero < max (abs down) (abs up)" $ abs zero < max (abs down) (abs up)
       , counterexample "not (isMantissaEven toOdd)" $ not (isMantissaEven toOdd)
       ]

prop_addToOdd :: RealFloat a => Proxy a -> a -> a -> Property
prop_addToOdd _ x y = isFinite x && isFinite y && isFinite (x + y) ==>
  let z = addToOdd x y
      w = if x == 0 && y == 0 then
            x + y
          else
            roundToOdd (fromRationalR (toRational x + toRational y))
  in z `sameFloatP` w

eachStrategy :: Testable prop => (forall f. RoundingStrategy f => (f a -> a) -> prop) -> Property
eachStrategy p = conjoin
  [ counterexample "roundTiesToEven" (p roundTiesToEven)
  , counterexample "roundTiesToAway" (p roundTiesToAway)
  , counterexample "roundTiesTowardZero" (p roundTiesTowardZero)
  , counterexample "roundTowardPositive" (p roundTowardPositive)
  , counterexample "roundTowardNegative" (p roundTowardNegative)
  , counterexample "roundTowardZero" (p roundTowardZero)
  , counterexample "roundToOdd" (p roundToOdd)
  ]

testUnary :: RealFloat b => (a -> b) -> [(String, a, b)] -> Property
testUnary f cases = conjoin
  [ counterexample t $ f a `sameFloatP` result
  | (t,a,result) <- cases
  ]

{-# NOINLINE spec #-}
spec :: Spec
spec = do
  describe "Double" $ do
    let proxy :: Proxy Double
        proxy = Proxy
    prop "fromIntegerR vs fromIntegralR" $ eachStrategy (prop_fromIntegerR_vs_fromIntegralR proxy (Proxy :: Proxy Int))
    prop "fromIntegerR vs fromIntegralR" $ eachStrategy (prop_fromIntegerR_vs_fromIntegralR proxy (Proxy :: Proxy Word64))
    prop "fromIntegerR vs fromRationalR" $ eachStrategy (prop_fromIntegerR_vs_fromRationalR proxy)
    prop "fromIntegerR vs encodeFloatR" $ eachStrategy (prop_fromIntegerR_vs_encodeFloatR proxy)
    prop "fromRationalR vs encodeFloatR" $ eachStrategy (prop_fromRationalR_vs_encodeFloatR proxy)
    prop "fromRationalR vs fromRational" $ prop_fromRationalR_vs_fromRational proxy
    prop "scaleFloatR vs fromRationalR" $ eachStrategy (prop_scaleFloatR_vs_fromRationalR proxy)
    prop "scaleFloatR vs encodeFloatR" $ eachStrategy (prop_scaleFloatR_vs_encodeFloatR proxy)
    prop "result of fromIntegerR" $ \x -> prop_order proxy (fromIntegerR x)
    prop "result of fromRationalR" $ \x -> prop_order proxy (fromRationalR x)
    prop "result of encodeFloatR" $ \m k -> prop_order proxy (encodeFloatR m k)
    prop "encodeFloatR/decodeFloat" $ forAllFloats $ \x -> eachStrategy (prop_encodeFloatR_roundtrip proxy x)
    prop "addToOdd" $ forAllFloats2 $ prop_addToOdd proxy
    it "fromIntegralR/(maxBound :: Int64)" $ fromIntegralTiesToEven (maxBound :: Int64) `sameFloatP` (0x1p63 :: Double)
    it "fromIntegralR/(maxBound :: Word64)" $ fromIntegralTiesToEven (maxBound :: Word64) `sameFloatP` (0x1p64 :: Double)
    it "fromIntegralR/(0xffff_ffff_ffff_fc00 :: Word64)" $ fromIntegralTiesToEven (0xffff_ffff_ffff_fc00 :: Word64) `sameFloatP` (0x1p64 :: Double)

    do let cases :: [(String, Rational, Double)]
           cases = [ let t = 11435996997111233 %  
                     in ('(' : shows t ")", t, 0.0)
                   ]
       prop "roundTiesToEven" $ testUnary (roundTiesToEven . fromRationalR) cases

    let cases :: [(String, Rational, Double)]
        cases = [("0x1.ffff_ffff_ffff_f8p1023", 0x1.ffff_ffff_ffff_f8p1023, maxFinite)
                ,("(0x1.ffff_ffff_ffff_f8p1023 + 1/723)", 0x1.ffff_ffff_ffff_f8p1023 + 1/723, 1/0)
                ,("(0x1.ffff_ffff_ffff_f8p1023 - 1/255)", 0x1.ffff_ffff_ffff_f8p1023 - 1/255, maxFinite)
                ,("0xdead_beef.8p-1074", 0xdead_beef.8p-1074, 0xdead_beefp-1074)
                ,("0xdead_beef.9p-1074", 0xdead_beef.9p-1074, 0xdead_bef0p-1074)
                ,("-0xdead_beef.7p-1074", -0xdead_beef.7p-1074, -0xdead_beefp-1074)
                ,("-0x0.8p-1074", -0x0.8p-1074, -0)
                ,("-0x0.80007p-1074", -0x0.80007p-1074, -0x1p-1074)
                ]
    prop "roundTiesTowardZero" $ testUnary (roundTiesTowardZero . fromRationalR) cases

  describe "Float" $ do
    let proxy :: Proxy Float
        proxy = Proxy
    prop "fromIntegerR vs fromRationalR" $ eachStrategy (prop_fromIntegerR_vs_fromRationalR proxy)
    prop "fromIntegerR vs encodeFloatR" $ eachStrategy (prop_fromIntegerR_vs_encodeFloatR proxy)
    prop "fromRationalR vs encodeFloatR" $ eachStrategy (prop_fromRationalR_vs_encodeFloatR proxy)
    prop "fromRationalR vs fromRational" $ prop_fromRationalR_vs_fromRational proxy
    prop "scaleFloatR vs fromRationalR" $ eachStrategy (prop_scaleFloatR_vs_fromRationalR proxy)
    prop "scaleFloatR vs encodeFloatR" $ eachStrategy (prop_scaleFloatR_vs_encodeFloatR proxy)
    prop "result of fromIntegerR" $ \x -> prop_order proxy (fromIntegerR x)
    prop "result of fromRationalR" $ \x -> prop_order proxy (fromRationalR x)
    prop "result of encodeFloatR" $ \m k -> prop_order proxy (encodeFloatR m k)
    prop "encodeFloatR/decodeFloat" $ forAllFloats $ \x -> eachStrategy (prop_encodeFloatR_roundtrip proxy x)
    prop "addToOdd" $ forAllFloats2 $ prop_addToOdd proxy
    it "fromIntegralR/(maxBound :: Int32)" $ fromIntegralTiesToEven (maxBound :: Int32) `sameFloatP` (0x1p31 :: Float)
    it "fromIntegralR/(maxBound :: Word32)" $ fromIntegralTiesToEven (maxBound :: Word32) `sameFloatP` (0x1p32 :: Float)
    it "fromIntegralR/(maxBound :: Int64)" $ fromIntegralTiesToEven (maxBound :: Int64) `sameFloatP` (0x1p63 :: Float)
    it "fromIntegralR/(maxBound :: Word64)" $ fromIntegralTiesToEven (maxBound :: Word64) `sameFloatP` (0x1p64 :: Float)
    it "fromIntegralR/(0xffff_ff80_0000_0000 :: Word64)" $ fromIntegralTiesToEven (0xffff_ff80_0000_0000 :: Word64) `sameFloatP` (0x1p64 :: Float)

    do let cases :: [(String, Rational, Float)]
           cases = [ let t = 20113311130255 % 822127761653273855988822146978202976557090789271144163906483851513046701868339517444102604474616762490976436939594169664101896669409817473587913461546435532885567073887954501607977104895740769882295378286300234464764201845440572849224022844453347299057834829757872072616746710668820893729486742297607776797874
                     in ('(' : shows t ")", t, 0.0)
                   ]
       prop "roundTiesToEven" $ testUnary (roundTiesToEven . fromRationalR) cases

    do let cases :: [(String, Rational, Float)]
           cases = [ ("0x1.ffff_ffp127", 0x1.ffff_ffp127, maxFinite)
                   , ("(0x1.ffff_ffp127 + 1/723)", 0x1.ffff_ffp127 + 1/723, 1/0)
                   , ("(0x1.ffff_ffp127 - 1/255)", 0x1.ffff_ffp127 - 1/255, maxFinite)
                   , ("0xbeef.8p-149", 0xbeef.8p-149, 0xbeefp-149)
                   , ("0xbeef.9p-149", 0xbeef.9p-149, 0xbef0p-149)
                   , ("-0xbeef.7p-149", -0xbeef.7p-149, -0xbeefp-149)
                   , ("-0x0.8p-149", -0x0.8p-149, -0)
                   , ("-0x0.80007p-149", -0x0.80007p-149, -0x1p-149)
                   ]
       prop "roundTiesTowardZero" $ testUnary (roundTiesTowardZero . fromRationalR) cases
