{-# LANGUAGE ExistentialQuantification,
             ScopedTypeVariables,
             FlexibleInstances,
             DeriveDataTypeable,
             TypeFamilies #-}

module Graphics.QML.Test.Framework where

import Graphics.QML.Objects
import Graphics.QML.Marshal
import Graphics.QML.Test.MayGen
import Graphics.QML.Test.ScriptDSL (Prog)
import qualified Graphics.QML.Test.ScriptDSL as S

import Test.QuickCheck.Gen
import Test.QuickCheck.Arbitrary
import Data.List (mapAccumR)
import Data.Maybe
import Data.Monoid
import Data.Proxy
import Data.Typeable
import Data.IORef
import Data.IntMap (IntMap)
import qualified Data.IntMap as IntMap

import Data.Int
import Data.Text (Text)
import qualified Data.Text as T

data TestType = forall a. (TestAction a) => TestType (Proxy a)

instance Eq TestType where
    (==) (TestType a) (TestType b) = typeOf a == typeOf b

instance Show TestType where
    showsPrec d (TestType p) =
        showParen (d > 10) $ showString "TestType (Proxy :: " .
            shows (typeOf p) . showString ")"

newtype Serial = Serial Int deriving (Show, Eq)

badSerial :: Serial
badSerial = Serial (-1)

data TestEnv = TestEnv {
    envSerial :: Serial,
    envHs     :: IntMap (TestType, Serial),
    envJs     :: IntMap (TestType, Serial)
} deriving Show

newTestEnv :: TestType -> TestEnv
newTestEnv tt =
    TestEnv (Serial 1) IntMap.empty (IntMap.singleton 0 (tt, Serial 0))

testEnvStep :: TestEnv -> TestEnv
testEnvStep (TestEnv (Serial s) h j) =
    TestEnv (Serial $ s+1) h j

testEnvSerial :: (Serial -> TestEnv -> TestEnv) -> TestEnv -> TestEnv
testEnvSerial f env = f (envSerial env) env

testEnvSetJ :: Int -> TestType -> Serial -> TestEnv -> TestEnv
testEnvSetJ n tt js (TestEnv s h j) =
    TestEnv s h (IntMap.insert n (tt,js) j)

testEnvIsaJ :: Int -> TestType -> TestEnv -> Bool
testEnvIsaJ n tt env =
    case IntMap.lookup n $ envJs env of
        Just (tt', _) -> tt' == tt
        _             -> False

testEnvNextJ :: TestEnv -> Int
testEnvNextJ = (+1) . fst . IntMap.findMax . envJs

testEnvListJ :: TestType -> TestEnv -> [Int]
testEnvListJ tt env =
    map fst $ filter (\(_,(tt',_)) ->
        tt' == tt) $ IntMap.toList $ envJs env

data TestBox = forall a. (TestAction a) => TestBox Int a

instance Show TestBox where
    showsPrec d (TestBox n a) =
        showParen (d > 10) $ showString "TestBox " . shows n .
            showString " " . showsPrec 11 a

testBoxType :: TestBox -> TestType
testBoxType (TestBox _ a) =
    TestType $ mkProxy a
    where mkProxy = const Proxy :: a -> Proxy a

class (Typeable a, Show a) => TestAction a where
    legalActionIn  :: a -> TestEnv -> Bool
    nextActionsFor :: TestEnv -> MayGen a
    updateEnvRaw   :: a -> TestEnv -> TestEnv
    actionRemote   :: a -> Int -> Prog
    mockObjDef     :: [Member (MockObj a)]

legalAction :: TestEnv -> TestBox -> Bool
legalAction env tb@(TestBox n a) =
    (fmap fst $ IntMap.lookup n $ envJs env) == Just (testBoxType tb) &&
        legalActionIn a env

nextActions :: TestEnv -> Gen TestBox
nextActions env =
    oneof $ mapMaybe (uncurry f) $ IntMap.toList $ envJs env
    where f n ((TestType pxy), _) =
              (mayGen $ fmap (TestBox n . flip asProxyTypeOf pxy) $
                  nextActionsFor env)

updateEnv :: TestBox -> TestEnv -> TestEnv
updateEnv (TestBox _ a) = updateEnvRaw a

showTestCode :: [TestBox] -> ShowS
showTestCode xs =
    let f (TestBox n a) = actionRemote a n
    in S.showProg $ mconcat $ map f xs ++ [S.end]

newtype TestBoxSrc a = TestBoxSrc { srcTestBoxes :: [TestBox]} deriving Show

genTestBoxes :: Int -> TestEnv -> Gen [TestBox]
genTestBoxes 0 _ = return []
genTestBoxes len env = do
    x <- nextActions env
    xs <- genTestBoxes (len-1) (updateEnv x env)
    return (x:xs)

partitions :: [a] -> [([a],[a])]
partitions xs =
    ([],xs) : case xs of
        []    -> []
        x:xs' -> map (\(hs,ts) -> (x:hs,ts)) $ partitions xs'

partitions1 :: [a] -> [([a],a,[a])]
partitions1 xs =
    mapMaybe f $ partitions xs
    where f (_,[]) = Nothing
          f (as,(b:cs)) = Just (as,b,cs)

pruneTestBoxes :: TestEnv -> [TestBox] -> [TestBox]
pruneTestBoxes env xs =
    catMaybes $ snd $ mapAccumR f env xs
    where f e x | legalAction e x = (updateEnv x e, Just x)
                | otherwise       = (e, Nothing)

shrinkHelper :: TestEnv -> [TestBox] -> [TestBox] -> [TestBox]
shrinkHelper env xs ys =
    let env' = foldr updateEnv env xs
        ys'  = pruneTestBoxes env' ys
    in xs ++ ys'

instance (TestAction a) => Arbitrary (TestBoxSrc a) where
    arbitrary =
        fmap TestBoxSrc $ sized $ \sz ->
            genTestBoxes sz $ newTestEnv $ TestType (Proxy :: Proxy a)
    shrink (TestBoxSrc xs) =
        map (\(a,_,b) -> TestBoxSrc $ shrinkHelper env a b) $ partitions1 xs
        where env = newTestEnv $ TestType (Proxy :: Proxy a)

mockFromSrc :: forall a. (TestAction a) => TestBoxSrc a -> IO (MockObj a)
mockFromSrc (TestBoxSrc ts) = do 
    statusRef <- newIORef $ TestStatus ts Nothing
        (newTestEnv $ TestType (Proxy :: Proxy a)) IntMap.empty
    return $ MockObj (Serial 0) statusRef

data TestFault
    = TOverAction
    | TUnderAction
    | TBadAction
    | TBadActionType
    | TBadActionCtor
    | TBadActionSlot
    | TBadActionData
    | TTimeout
    | TInvalid
    deriving Show

newtype Anything = Anything () deriving Show

data TestStatus = TestStatus {
    testList  :: [TestBox],
    testFault :: Maybe TestFault,
    testEnv   :: TestEnv,
    testObjs  :: IntMap Anything
} deriving Show

testSerial :: TestStatus -> Serial
testSerial = envSerial . testEnv

data MockObj a = MockObj {
    mockSerial :: Serial,
    mockStatus :: IORef TestStatus
} deriving Typeable

mockGetStatus :: MockObj a -> IO TestStatus
mockGetStatus (MockObj _ statusRef) = readIORef statusRef

class MakeDefault a where
    makeDef :: IO a

instance (TestAction a) => MakeDefault (ObjRef (MockObj a)) where
    makeDef = do
        statusRef <- newIORef $ TestStatus [] (Just TInvalid)
            (TestEnv badSerial IntMap.empty IntMap.empty) IntMap.empty
        newObjectDC $ MockObj badSerial statusRef

instance MakeDefault () where
    makeDef = return ()

instance MakeDefault Bool where
    makeDef = return False

instance MakeDefault Int32 where
    makeDef = return 0

instance MakeDefault Double where
    makeDef = return (0/0)

instance MakeDefault [a] where
    makeDef = return []

instance MakeDefault Text where
    makeDef = return T.empty

instance MakeDefault (Maybe a) where
    makeDef = return Nothing

expectAction :: (TestAction a, MakeDefault b) =>
    MockObj a -> (a -> IO (Either TestFault b)) -> IO b
expectAction mock pred = do
    status <- readIORef $ mockStatus mock
    res <- case status of
        TestStatus (b:_) Nothing env _ -> case b of
            TestBox _ a -> case cast a of
                Just a' -> pred a' >>= return . fmap ((,) (updateEnvRaw a' env))
                _       -> return $ Left TBadActionType
        TestStatus [] Nothing _ _       -> return $ Left TOverAction
        TestStatus _ (Just f) _ _       -> return $ Left f
    case res of
        Left f  -> do
            let (TestStatus bs _ env objs) = status
            writeIORef (mockStatus mock) $ TestStatus bs (Just f) env objs
            makeDef
        Right (env', v) -> do
            let (TestStatus (_:bs) _ _ objs) = status
            writeIORef (mockStatus mock) $ TestStatus bs Nothing env' objs
            return v

expectActionRef :: (TestAction a, MakeDefault b) =>
    ObjRef (MockObj a) -> (a -> IO (Either TestFault b)) -> IO b
expectActionRef ref pred = expectAction (fromObjRef ref) pred

checkAction :: (TestAction a, Eq a, MakeDefault b) =>
    MockObj a -> a -> IO b -> IO b
checkAction mock action next = expectAction mock $ \expected -> do
    if expected == action
    then fmap Right $ next
    else return . Left $
        if fakeToConstr expected /= fakeToConstr action
        then TBadActionCtor
        else TBadActionData
    where fakeToConstr = takeWhile (/= ' ') . show

badAction :: (MakeDefault b) => MockObj a -> IO b
badAction mock = do
    status <- readIORef $ mockStatus mock
    writeIORef (mockStatus mock) $ status {testFault = Just TBadAction} 
    makeDef

forkMockObj :: (TestAction b) => MockObj a -> IO (ObjRef (MockObj b))
forkMockObj m = do
    status <- mockGetStatus m
    newObjectDC $ MockObj (testSerial status) $ mockStatus m

checkMockObj :: forall a b. (TestAction b) =>
    MockObj a -> MockObj b -> Int -> IO (Either TestFault ())
checkMockObj m v w = do
    status <- mockGetStatus m
    case IntMap.lookup w $ envJs $ testEnv status of
        Just entry -> if entry == (TestType (Proxy :: Proxy b), mockSerial v)
                      then return $ Right ()
                      else return $ Left TBadActionData
        _          -> return $ Left TBadActionSlot

instance (TestAction a) => DefaultClass (MockObj a) where
    classMembers = mockObjDef

instance (TestAction a) => Marshal (MockObj a) where
    type MarshalMode (MockObj a) c d = ModeObjFrom (MockObj a) c
    marshaller = fromMarshaller fromObjRef