{-# LANGUAGE BangPatterns #-}
module Action where

import Control.Applicative
import Test.QuickCheck

import qualified Data.ByteString.Lazy as L

import qualified Data.Binary.Get as Binary

import Arbitrary()

data Action
  = GetByteString Int
  | Try [Action] [Action]
  | BytesRead
  | Fail
  deriving (Show, Eq)

instance Arbitrary Action where
  shrink action =
    case action of
      GetByteString n -> [ GetByteString n' | n' <- shrink n, n > 0 ]
      BytesRead -> []
      Fail -> []
      Try a b ->
        [ Try a' b' | a' <- shrink a, b' <- shrink b ]
        ++ [ Try a' b | a' <- shrink a ]
        ++ [ Try a b' | b' <- shrink b ]

willFail :: [Action] -> Bool
willFail [] = False
willFail (x:xs) =
  case x of
    GetByteString _ -> willFail xs
    Try a b -> (willFail a && willFail b) || willFail xs
    BytesRead -> willFail xs
    Fail -> True

max_len :: [Action] -> Int
max_len [] = 0
max_len (x:xs) =
  case x of
    GetByteString n -> n + max_len xs
    BytesRead -> max_len xs
    Fail -> 0
    Try a b -> max (max_len a) (max_len b) + max_len xs

actual_len :: [Action] -> Maybe Int
actual_len = go 0
  where
  go !s [] = Just s
  go !s (x:xs) =
    case x of
      GetByteString n -> go (s+n) xs
      Fail -> Nothing
      BytesRead -> go s xs
      Try a b | not (willFail a) -> liftA2 (+) (go s a) (actual_len xs)
              | not (willFail b) -> liftA2 (+) (go s b) (actual_len xs)
              | otherwise -> Nothing

-- | Build binary programs and compare running them to running a (hopefully)
-- identical model.
-- Tests that 'bytesRead' returns correct values when used together with '<|>'
-- and 'fail'.
prop_action :: Property
prop_action =
  forAllShrink gen_actions shrink $ \ actions ->
    forAll arbitrary $ \ lbs ->
      L.length lbs >= fromIntegral (max_len actions) ==>
        case Binary.runGet (eval actions) lbs of
          () -> True

eval :: [Action] -> Binary.Get ()
eval = go 0
  where
  go _ [] = return ()
  go pos (x:xs) =
    case x of
      GetByteString n ->
        Binary.getByteString n >> go (pos+n) xs
      BytesRead -> do
        pos' <- Binary.bytesRead
        if (pos == fromIntegral pos')
          then go pos xs
          else error $ "expected " ++ show pos ++ " but got " ++ show pos'
      Fail -> fail "fail"
      Try a b -> do
        len <- leg pos a <|> leg pos b
        case len of
          Nothing -> error "got Nothing, but we're still here..."
          Just offset -> go (pos+offset) xs
  leg pos t = go pos t >> return (actual_len t)

gen_actions :: Gen [Action]
gen_actions = sized (go False)
  where
  go :: Bool -> Int -> Gen [Action]
  go     _ 0 = return []
  go inTry s = oneof $ [ do n <- choose (0,10)
                            (:) (GetByteString n) <$> go inTry (s-1)
                       , do (:) BytesRead <$> go inTry (s-1)
                       , do t1 <- go True (s `div` 2)
                            t2 <- go inTry (s `div` 2)
                            (:) (Try t1 t2) <$> go inTry (s `div` 2)
                       ] ++ [ return [Fail] | inTry ]
