{-# LANGUAGE ScopedTypeVariables #-}
module Main (main) where

import Control.Exception (evaluate)
import Data.List (elemIndex)
import Data.Int (Int8)
import Test.QuickCheck
import Data.Universe.Class (Universe(..), Finite(..))
import Data.Universe.Helpers (interleave, choices)
import Data.Set (Set)
import Data.Ratio (Ratio, (%))
import Numeric.Natural (Natural)
import System.Timeout (timeout)

import qualified Data.Set as Set

data P a = P

-------------------------------------------------------------------------------
-- Universe laws
-------------------------------------------------------------------------------

universeLaw :: (Eq a, Show a, Arbitrary a, Universe a) => P a -> a -> Property
universeLaw _ x = counterexample (show x) (elem x universe)

universeProdLaw
    :: forall a. (Ord a, Show a, Arbitrary a, Universe a)
    => P a -> NonNegative Int -> Property
universeProdLaw _ (NonNegative n) = label (show $ div n 10) $
    let pfx = take n universe :: [a]
    in length pfx === nubLength pfx

nubLength :: Ord a => [a] -> Int
nubLength = Set.size . Set.fromList

universeLaws :: (Ord a, Show a, Arbitrary a, Universe a) => P a -> Property
universeLaws p = universeLaw p .&&. universeProdLaw p

rationalLaw :: Gen Property
-- We have to keep the numbers fairly small here to avoid needing to
-- dig too deep.
rationalLaw = do
  n <- choose (-19, 19 :: Integer)
  d <- choose (1, 19)
  return $ let nd = n % d in counterexample (show nd) (elem nd universe)

natRatioLaw :: Gen Property
natRatioLaw = do
  n <- choose (0, 19 :: Int)
  d <- choose (1, 19 :: Int)
  return $ let nd = (fromIntegral n :: Natural) % fromIntegral d
           in counterexample (show nd) (elem nd universe)

-------------------------------------------------------------------------------
-- Finite laws
-------------------------------------------------------------------------------

finiteLaw1 :: (Eq a, Show a, Arbitrary a, Finite a) => P a -> a -> Property
finiteLaw1 _ x = counterexample (show x) (elem x universeF)

finiteLaw2 :: (Eq a, Show a, Arbitrary a, Finite a) => P a -> a -> Property
finiteLaw2 _ x = length (filter (== x) universeF) === 1

finiteLaws :: (Ord a, Show a, Arbitrary a, Finite a) => P a -> Property
finiteLaws p = universeLaws p .&&. finiteLaw1 p .&&. finiteLaw2 p

-------------------------------------------------------------------------------
-- Special examples
-------------------------------------------------------------------------------

eitherExample :: Property
eitherExample = once $ u /= f
  where
    u = elemIndex (Left True :: Either Bool Bool) universe
    f = elemIndex (Left True :: Either Bool Bool) universeF

choicesLazinessProperty :: IO ()
choicesLazinessProperty = do
    v <- timeout oneSecond (evaluate (s !! 1))
    case v of
        Just _ -> putStrLn "OK"
        Nothing -> putStrLn "ERROR: Timeout while evaluating a sneaky, self-referential collection of helpers"
    where
    -- generate strings from the grammar S -> x | S S
    s = interleave [["x"], map concat $ choices [s, s]]
    oneSecond = 1000000

-------------------------------------------------------------------------------
-- Main
-------------------------------------------------------------------------------

main :: IO ()
main = do
    -- Note: checking on 'Int' is bad idea as it's definition is 'universeDef',
    -- i.e. it takes lots of time to get to small numbers!
    quickCheck eitherExample
    quickCheck $ universeLaws (P :: P Integer)
    quickCheck $ universeLaws (P :: P Natural')
    quickCheck $ rationalLaw
    quickCheck $ natRatioLaw
    quickCheck $ universeProdLaw (P :: P Rational)
    quickCheck $ universeProdLaw (P :: P (Ratio Natural))
    quickCheck $ finiteLaws (P :: P Char)
    quickCheck $ finiteLaws (P :: P (Maybe Int8))
    quickCheck $ finiteLaws (P :: P (Either Int8 Int8))
    -- Even this is a bad idea:
    -- quickCheck $ universeLaw (P :: P [Bool])

    quickCheck $ universeProdLaw (P :: P (Set Integer))
    quickCheck $ finiteLaws (P :: P (Set ()))
    quickCheck $ finiteLaws (P :: P (Set Bool))
    quickCheck $ finiteLaws (P :: P (Set (Maybe Bool)))
    quickCheck $ finiteLaws (P :: P (Set (Set (Maybe Bool))))

    choicesLazinessProperty

-------------------------------------------------------------------------------
-- Natural'
-------------------------------------------------------------------------------

newtype Natural' = Natural' Natural deriving (Eq, Ord, Show)
instance Universe Natural' where universe = map Natural' universe
instance Arbitrary Natural' where
    arbitrary = fmap (Natural' . fromInteger . abs) arbitrary
