{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE TypeApplications #-}

module Data.Units.Base.ArithmeticProp where

import Data.Coerce

import Test.QuickCheck
import Test.Hspec

import Data.Epsilon

import Data.Units

import Data.Units.Base.ConvertProp


------------------------- Num and fractional instances -------------------------

addNumProp :: forall u a.
  ( IsUnit u, Floating (u a)
  , Show a, Epsilon a, Floating a, Eq a, Arbitrary a) =>  Property
addNumProp = property (\ (a :: a) (b :: a) ->
  a + b + pi ==
    coerce (coerce a + coerce b + pi :: u a)
  )

divNumProp :: forall u a.
 ( IsUnit u, Fractional (u a)
 , Show a, Epsilon a, Fractional a, Eq a, Arbitrary a)
 => Property
divNumProp = property (\ (a :: a) (b :: a) -> b == 0 ||
  a / b ==
    coerce (coerce a / coerce b :: u a))

floatingSpec :: forall u a.
  ( IsUnit u, Floating (u a), ShowUnit u
  , Show a, Epsilon a, Floating a, Eq a, Arbitrary a
  )
  =>  Spec
floatingSpec = it ("Floating instance for " ++ showUnit @u ) $
  addNumProp @u @a .&&. divNumProp @u @a


-------------------------- Arithmetic same dimension ---------------------------



addRight :: forall u v a.
  ( FromTo' v u a
  )
  => a -> a -> a
addRight u v = coerce $ (coerce u :: u a) .+~ (coerce v :: v a)

addRightProp :: forall u v a.
  ( FromTo' v u a
  , DimEq u v
  , Show a, Epsilon a, Arbitrary a
  )
  => Property
addRightProp = property (\(a :: a) (b :: a) ->
  aboutEqual (coerce (fromTo' (coerce a :: u a) :: v a) + b :: a)
             (addRight @u @v a b))

addRightSpec :: forall u v a.
  ( FromTo' v u a
  , DimEq u v
  , ShowUnit u, ShowUnit v
  , Show a, Epsilon a, Arbitrary a
  ) => Spec
addRightSpec = it (showUnit @u ++ " .+~ " ++ showUnit @v)
  $ addRightProp @u @v @a

addLeft :: forall u v a.
  ( FromTo' u v a
  )
  => a -> a -> a
addLeft u v = coerce $ (coerce u :: u a) ~+. (coerce v :: v a)

addLeftProp :: forall u v a.
  ( FromTo' u v a
  , DimEq u v
  , Show a, Epsilon a, Arbitrary a
  )
  => Property
addLeftProp = property (\(a :: a) (b :: a) ->
  aboutEqual (coerce (fromTo' (coerce a :: u a) :: v a) + b :: a)
             (addLeft @u @v a b))

addLeftSpec :: forall u v a.
  ( FromTo' u v a
  , DimEq u v
  , ShowUnit u, ShowUnit v
  , Show a, Epsilon a, Arbitrary a
  ) => Spec
addLeftSpec = it (showUnit @u ++ " ~+. " ++ showUnit @v)
  $ addLeftProp @u @v @a


addSame :: forall u v a.
  FromTo' u v a
  => a -> a -> a
addSame u v = coerce $ (coerce u :: u a) ~+~ (coerce v :: v a)

addSameProp :: forall u v a.
  ( FromTo' u v a
  , DimEq u v
  , Show a, Epsilon a, Arbitrary a
  )
  => Property
addSameProp = property (\(a :: a) (b :: a) ->
  aboutEqual (coerce (fromTo' (coerce a :: u a) :: v a) + b :: a)
             (addSame @u @v a b))

addSameSpec :: forall u v a.
  ( FromTo' u v a
  , DimEq u v
  , ShowUnit u, ShowUnit v
  , Show a, Epsilon a, Arbitrary a
  ) => Spec
addSameSpec = it (showUnit @u ++ " ~+~ " ++ showUnit @v)
  $ addSameProp @u @v @a

--------------------------------- Subtraction ----------------------------------

subRight :: forall u v a.
  ( FromTo' v u a
  )
  => a -> a -> a
subRight u v = coerce $ (coerce u :: u a) .-~ (coerce v :: v a)

subRightProp :: forall u v a.
  ( FromTo' v u a
  , DimEq u v
  , Show a, Epsilon a, Arbitrary a
  )
  => Property
subRightProp = property (\(a :: a) (b :: a) ->
  aboutEqual (coerce (fromTo' (coerce a :: u a) :: v a) - b :: a)
             (subRight @u @v a b))

subRightSpec :: forall u v a.
  ( FromTo' v u a
  , DimEq u v
  , ShowUnit u, ShowUnit v
  , Show a, Epsilon a, Arbitrary a
  ) => Spec
subRightSpec = it (showUnit @u ++ " .-~ " ++ showUnit @v)
  $ subRightProp @u @v @a

subLeft :: forall u v a.
  ( FromTo' u v a
  )
  => a -> a -> a
subLeft u v = coerce $ (coerce u :: u a) ~-. (coerce v :: v a)

subLeftProp :: forall u v a.
  ( FromTo' u v a
  , DimEq u v
  , Show a, Epsilon a, Arbitrary a
  )
  => Property
subLeftProp = property (\(a :: a) (b :: a) ->
  aboutEqual (coerce (fromTo' (coerce a :: u a) :: v a) - b :: a)
             (subLeft @u @v a b))

subLeftSpec :: forall u v a.
  ( FromTo' u v a
  , DimEq u v
  , ShowUnit u, ShowUnit v
  , Show a, Epsilon a, Arbitrary a
  ) => Spec
subLeftSpec = it (showUnit @u ++ " ~-. " ++ showUnit @v)
  $ subLeftProp @u @v @a


subSame :: forall u v a.
  FromTo' u v a
  => a -> a -> a
subSame u v = coerce $ (coerce u :: u a) ~-~ (coerce v :: v a)

subSameProp :: forall u v a.
  ( FromTo' u v a
  , DimEq u v
  , Show a, Epsilon a, Arbitrary a
  )
  => Property
subSameProp = property (\(a :: a) (b :: a) ->
  aboutEqual (coerce (fromTo' (coerce a :: u a) :: v a) - b :: a)
             (subSame @u @v a b))

subSameSpec :: forall u v a.
  ( FromTo' u v a
  , DimEq u v
  , ShowUnit u, ShowUnit v
  , Show a, Epsilon a, Arbitrary a
  ) => Spec
subSameSpec = it (showUnit @u ++ " ~-~ " ++ showUnit @v)
  $ subSameProp @u @v @a


-------------------------------- Multiplication --------------------------------

mulSame :: forall u v a u2 .
  ( u2 ~ BaseUnitOf u .^+ 2, IsUnit u2
  , DimEq u v
  , ConversionFactor u a, ConversionFactor v a
  )
  => a -> a -> a
mulSame u v = coerce $ (coerce u :: u a) ~*~ (coerce v :: v a)

mulSameProp :: forall u v a u2 .
  ( u2 ~ BaseUnitOf u .^+ 2, IsUnit u2
  , DimEq u v
  , ConversionFactor u a, ConversionFactor v a
  , Show a, Epsilon a, Arbitrary a
  )
  => Property
mulSameProp = property (\(a :: a) (b :: a) ->
  aboutEqual (coerce (fromTo' (coerce a :: u a) :: v a) * b :: a)
             (mulSame @u @v a b))

mulSameSpec :: forall u v a u2 .
  ( u2 ~ BaseUnitOf u .^+ 2, IsUnit u2
  , DimEq u v
  , ConversionFactor u a, ConversionFactor v a
  , ShowUnit u, ShowUnit v
  , Show a, Epsilon a, Arbitrary a
  )
  => Spec
mulSameSpec = it (showUnit @u ++ " ~*~ " ++ showUnit @v)
  $ mulSameProp @u @v @a

divSame :: forall u v a u2 .
  ( u2 ~ BaseUnitOf u .^+ 2, IsUnit u2
  , DimEq u v
  , ConversionFactor u a, ConversionFactor v a
  )
  => a -> a -> a
divSame u v = coerce $ (coerce u :: u a) ~/~ (coerce v :: v a)

divSameProp :: forall u v a u2 .
  ( u2 ~ BaseUnitOf u .^+ 2, IsUnit u2
  , DimEq u v
  , ConversionFactor u a, ConversionFactor v a
  , Show a, Epsilon a, Eq a, Arbitrary a
  )
  => Property
divSameProp = property (\(a :: a) (b :: a) -> b == 0 ||
  aboutEqual (coerce (fromTo' (coerce a :: u a) :: v a) / b :: a)
             (divSame @u @v a b))

divSameSpec :: forall u v a u2 .
  ( u2 ~ BaseUnitOf u .^+ 2, IsUnit u2
  , DimEq u v
  , ConversionFactor u a, ConversionFactor v a
  , ShowUnit u, ShowUnit v
  , Show a, Epsilon a, Eq a, Arbitrary a
  )
  => Spec
divSameSpec = it (showUnit @u ++ " ~/~ " ++ showUnit @v)
  $ divSameProp @u @v @a

------------------ Arithmetic with two different dimensions  -------------------

-- mulRight :: forall u v a u2.
--   ( u2 ~ BaseUnitOf' (u .^+ 2) , IsUnit u2
--   , DimEq u v
--   , FromTo' v u a
--   )
--   => a -> a -> a
-- mulRight u v = coerce $ (coerce u :: u a) .*~ (coerce v :: v a)

-- mulRightProp :: forall u v a u2.
--   ( u2 ~ BaseUnitOf' (u .^+ 2) , IsUnit u2
--   , DimEq u v
--   , FromTo' v u a
--   , Show a, Epsilon a, Arbitrary a
--   )
--   => Property
-- mulRightProp = property (\(a :: a) (b :: a) ->
--   aboutEqual (coerce (fromTo' (coerce a :: u a) :: v a) * b :: a)
--              (mulRight @u @v a b))

-- mulRightSpec :: forall u v a u2.
--   ( u2 ~ BaseUnitOf' (u .^+ 2) , IsUnit u2
--   , DimEq u v
--   , FromTo' v u a
--   , ShowUnit u, ShowUnit v
--   , Show a, Epsilon a, Arbitrary a
--   ) => Spec
-- mulRightSpec = it (showUnit @u ++ " .*~ " ++ showUnit @v)
--   $ mulRightProp @u @v @a

-- mulLeft :: forall u v a v2.
--   ( v2 ~ BaseUnitOf' (v .^+ 2), IsUnit v2
--   , DimEq v u
--   , FromTo' u v a
--   )
--   => a -> a -> a
-- mulLeft u v = coerce $ (coerce u :: u a) ~*. (coerce v :: v a)

-- mulLeftProp :: forall u v a v2.
--   ( v2 ~ BaseUnitOf' (v .^+ 2), IsUnit v2
--   , DimEq v u
--   , FromTo' u v a
--   , Show a, Epsilon a, Arbitrary a
--   )
--   => Property
-- mulLeftProp = property (\(a :: a) (b :: a) ->
--   aboutEqual (coerce (fromTo' (coerce a :: u a) :: v a) * b :: a)
--              (mulLeft @u @v a b))

-- mulLeftSpec :: forall u v a v2.
--   ( v2 ~ BaseUnitOf' (v .^+ 2), IsUnit v2
--   , DimEq v u
--   , FromTo' u v a
--   , ShowUnit u, ShowUnit v
--   , Show a, Epsilon a, Arbitrary a
--   ) => Spec
-- mulLeftSpec = it (showUnit @u ++ " ~*. " ++ showUnit @v)
--   $ mulLeftProp @u @v @a


mulDiffDim :: forall u v a.
  ( ConversionFactor u a, ConversionFactor v a)
  => a -> a -> a
mulDiffDim u v = coerce $ (coerce u :: u a) .*. (coerce v :: v a)

mulDiffDimProp :: forall u v a.
  ( ConversionFactor u a, ConversionFactor v a
  , ConversionFactor (u .*. v) a
  , Arbitrary a, Show a, Epsilon a
  )
  => Property
mulDiffDimProp =
  property (\a b -> aboutEqual (a * b) (mulDiffDim @u @v @a a b) )
  .&&. toFromProp @(u .*. v) @a

mulDiffDimSpec :: forall u v a.
  ( ConversionFactor u a, ConversionFactor v a
  , ConversionFactor (u .*. v) a
  , Arbitrary a, Show a, Epsilon a
  , ShowUnit u, ShowUnit v
  )
  => Spec
mulDiffDimSpec = it (showUnit @u ++ " .*. " ++ showUnit @v)
    $ mulDiffDimProp @u @v @a

divDiffDim :: forall u v a.
  ( IsUnit u, IsUnit v, IsUnit (u ./. v)
  , Fractional a
  )
  => a -> a -> a
divDiffDim u v = coerce $ (coerce u :: u a) ./. (coerce v :: v a)

divDiffDimProp :: forall u v a.
  ( IsUnit u, IsUnit v, IsUnit (u ./. v)
  , ConversionFactor (u ./. v) a
  , IsUnit (u ./. v)
  , Arbitrary a, Show a, Epsilon a, Eq a, Fractional a
  )
  => Property
divDiffDimProp =
  property (\a b -> b == 0 || aboutEqual (a / b) (divDiffDim @u @v @a a b) )
  .&&. toFromProp @(u ./. v) @a

divDiffDimSpec :: forall u v a.
  (IsUnit u, IsUnit v, IsUnit (u ./. v)
  , ConversionFactor (u ./. v) a
  , IsUnit (u ./. v)
  , Arbitrary a, Show a, Epsilon a, Eq a, Fractional a
  , ShowUnit u, ShowUnit v
  )
  => Spec
divDiffDimSpec =  it (showUnit @u ++ " ./. " ++ showUnit @v)
  $ divDiffDimProp @u @v @a

-------------------------------- Exponentiation --------------------------------

exp2 :: forall u a.
  ConversionFactor u a
  => a -> a
exp2 u = coerce $ (coerce u :: u a) .^. pos2

expm1 :: forall u a.
  ConversionFactor u a
  => a -> a
expm1 u = coerce $ (coerce u :: u a) .^. neg1

exp2Prop :: forall u a.
  ( ConversionFactor u a
  , Arbitrary a, Show a, Epsilon a
  )
  => Property
exp2Prop =
  property (\a -> aboutEqual (a * a) (exp2 @u @a a))

expm1Prop :: forall u a.
  ( ConversionFactor u a
  , Arbitrary a, Show a,  Eq a, Epsilon a
  )
  => Property
expm1Prop =
  property (\a -> a == 0 || aboutEqual (1 / a) (expm1 @u @a a))

expSpec :: forall u a.
  ( ConversionFactor u a
  , ShowUnit u
  , Arbitrary a, Show a,  Eq a, Epsilon a
  )
  => Spec
expSpec = it (showUnit @u ++ " .^+ 2  ," ++ showUnit @u ++ " .^- 1") $
  exp2Prop @u @a .&&. expm1Prop @u @a

exp2Conv :: forall u a.
  ConversionFactor u a
  => a -> a
exp2Conv u = coerce $ (coerce u :: u a) ~^~ pos2

expm1Conv :: forall u a.
  ConversionFactor u a
  => a -> a
expm1Conv u = coerce $ (coerce u :: u a) ~^~ neg1

exp2ConvProp :: forall u a.
  ( ConversionFactor u a
  , Arbitrary a, Show a, Epsilon a
  )
  => Property
exp2ConvProp =
  property (\a -> aboutEqual
    (coerce (toBaseUnit' (coerce a :: u a)) ^^ (2 :: Int)) (exp2Conv @u @a a))

expm1ConvProp :: forall u a.
  ( ConversionFactor u a
  , Arbitrary a, Show a, Eq a, Epsilon a
  )
  => Property
expm1ConvProp =
  property (\a -> a == 0 ||
    aboutEqual (coerce (toBaseUnit' (coerce a :: u a)) ^^ (-1 :: Int)) (expm1Conv @u @a a))


expConvSpec :: forall u a.
  ( ConversionFactor u a
  , ShowUnit u
  , Arbitrary a, Show a,  Eq a, Epsilon a
  )
  => Spec
expConvSpec = it (showUnit @u ++ "~^~ pos2  ," ++ showUnit @u ++ "~^~ neg1") $
  exp2ConvProp @u @a .&&. expm1ConvProp @u @a

