{-# OPTIONS_GHC -Wall #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE FlexibleInstances #-}

module Main ( main ) where

import Control.Applicative
import qualified Data.Foldable as F
import qualified Data.Monoid as Mo
import Test.Framework
       ( Test, ColorMode(..), RunnerOptions'(..), TestOptions'(..)
       , defaultMainWithOpts, testGroup )
import Test.Framework.Providers.QuickCheck2 ( testProperty )
import Test.QuickCheck
import Test.QuickCheck.Arbitrary
-- import Test.QuickCheck.Gen
import Text.Printf ( printf )

import SpatialMath

main :: IO ()
main = defaultMainWithOpts tests opts

close :: forall f . (F.Foldable f, Applicative f) => Double -> f Double -> f Double -> Maybe Double
close eps f0 f1
  | all (\x -> abs x <= eps) deltas = Nothing
  | otherwise = Just $ maximum $ map abs deltas
  where
    delta :: f Double
    delta = (-) <$> f0 <*> f1

    deltas = F.toList delta

closeDcm :: Double -> M33 Double -> M33 Double -> Maybe Double
closeDcm eps f0 f1
  | all (\x -> abs x <= eps) deltas = Nothing
  | otherwise = Just $ maximum $ map abs deltas
  where
    delta :: V3 (V3 Double)
    delta = (-) <$> f0 <*> f1

    deltas = concatMap F.toList (F.toList delta)

instance Arbitrary (Euler Double) where
  arbitrary = do
    yaw <- choose (-0.99*pi, 0.99*pi)
    pitch <- choose (-0.9*pi/2, 0.9*pi/2)
    roll <- choose (-0.99*pi, 0.99*pi)
    return
      Euler
      { eYaw = yaw
      , ePitch = pitch
      , eRoll = roll
      }

instance Arbitrary (Quaternion Double) where
--  arbitrary = quatOfEuler321 <$> arbitrary
  arbitrary = do
    w <- arbitrary
    x <- arbitrary
    y <- arbitrary
    z <- arbitrary
    let norm = sqrt (w*w + x*x + y*y + z*z)
        ret
          | norm == 0 =
              elements
              [ Quaternion 1 (V3 0 0 0)
              , Quaternion 0 (V3 1 0 0)
              , Quaternion 0 (V3 0 1 0)
              , Quaternion 0 (V3 0 0 1)
              , Quaternion (-1) (V3 0 0 0)
              , Quaternion 0 (V3 (-1) 0 0)
              , Quaternion 0 (V3 0 (-1) 0)
              , Quaternion 0 (V3 0 0 (-1))
              ]
          | otherwise = return $ Quaternion (w/norm) (V3 (x/norm) (y/norm) (z/norm))
    ret

instance Arbitrary (V3 (V3 Double)) where
  arbitrary = dcmOfEuler321 <$> arbitrary

testConversion :: (F.Foldable f, Applicative f, Show (f Double))
                  => Double -> (f Double -> f Double) -> f Double
                  -> Property
testConversion eps f x0 = counterexample msg ret
  where
    (ret, errmsg) = case close eps x0 x1 of
      Nothing -> (True, [])
      Just worstErr -> (False, [printf "worst error: %.3g" worstErr])
    msg = init $ unlines $
          [ "original:  " ++ show x0
          , "converted: " ++ show x1
          ] ++ errmsg
    x1 = f x0

prop_e2q2e :: Euler Double -> Property
prop_e2q2e = testConversion 1e-9 (euler321OfQuat . quatOfEuler321)

prop_e2d2e :: Euler Double -> Property
prop_e2d2e = testConversion 1e-9 (euler321OfDcm . dcmOfEuler321)

testDoubleConversion :: (Show f, Show g) => f -> g -> g -> Maybe Double -> Property
testDoubleConversion orig res0 res1 err = counterexample msg ret
  where
    (ret, errmsg) = case err of
      Nothing -> (True, [])
      Just worstErr -> (False, [printf "worst error: %.3g" worstErr])
    msg = init $ unlines $
          [ "original: " ++ show orig
          , "first route:  " ++ show res0
          , "second route: " ++ show res1
          ] ++ errmsg

prop_e2d_e2q2d :: Euler Double -> Property
prop_e2d_e2q2d euler = testDoubleConversion euler dcm0 dcm1 (closeDcm 1e-9 dcm0 dcm1)
  where
    dcm0 = dcmOfEuler321 euler
    dcm1 = dcmOfQuat (quatOfEuler321 euler)

prop_e2q_e2d2q :: Euler Double -> Property
prop_e2q_e2d2q euler = testDoubleConversion euler quat0 quat1 (close 1e-9 quat0 quat1)
  where
    quat0 = quatOfEuler321 euler
    quat1 = quatOfDcm (dcmOfEuler321 euler)

prop_q2e_q2d2e :: Quaternion Double -> Property
prop_q2e_q2d2e quat = testDoubleConversion quat euler0 euler1 (close 1e-9 euler0 euler1)
  where
    euler0 = euler321OfQuat quat
    euler1 = euler321OfDcm (dcmOfQuat quat)

prop_q2d_q2e2d :: Quaternion Double -> Property
prop_q2d_q2e2d quat = testDoubleConversion quat dcm0 dcm1 (closeDcm 1e-9 dcm0 dcm1)
  where
    dcm0 = dcmOfQuat quat
    dcm1 = dcmOfEuler321 (euler321OfQuat quat)

prop_d2e_d2q2e :: M33 Double -> Property
prop_d2e_d2q2e dcm = testDoubleConversion dcm euler0 euler1 (close 1e-7 euler0 euler1)
  where
    euler0 = euler321OfDcm dcm
    euler1 = euler321OfQuat (quatOfDcm dcm)

prop_d2q_d2e2q :: M33 Double -> Property
prop_d2q_d2e2q dcm = testDoubleConversion dcm quat0 quat1 (close 1e-6 quat0 quat1)
  where
    quat0 = quatOfDcm dcm
    quat1 = quatOfEuler321 (euler321OfDcm dcm)

tests :: [Test]
tests =
  [ testGroup "inverses"
    [ testProperty "(euler -> quat -> euler) == euler" prop_e2q2e
    , testProperty "(euler -> dcm -> euler) == euler" prop_e2d2e
    ]
  , testGroup "two routes"
    [ testProperty "(euler -> dcm) == (euler -> quat -> dcm)" prop_e2d_e2q2d
    , testProperty "(euler -> quat) == (euler -> dcm -> quat)" prop_e2q_e2d2q
    , testProperty "(quat -> euler) == (quat -> dcm -> euler)" prop_q2e_q2d2e
    , testProperty "(quat -> dcm) == (quat -> euler -> dcm)" prop_q2d_q2e2d
    , testProperty "(dcm -> euler) == (dcm -> quat -> euler)" prop_d2e_d2q2e
    , testProperty "(dcm -> quat) == (dcm -> euler -> quat)" prop_d2q_d2e2q
    ]
  ]

opts :: RunnerOptions' Maybe
opts =
  Mo.mempty
  { ropt_color_mode = Just ColorAlways
  , ropt_threads = Just 1
  , ropt_test_options = Just my_test_opts
  }

my_test_opts :: TestOptions' Maybe
my_test_opts =
  Mo.mempty
  { topt_timeout = Just (Just 15000000)
  }
