{-# LANGUAGE FlexibleContexts #-}

module Test.SmartCheck.ConstructorGen
  ( constrsGen
  ) where

import Test.SmartCheck.Args
import Test.SmartCheck.Types
import Test.SmartCheck.DataToTree
import Test.SmartCheck.SmartGen
import Test.SmartCheck.Render

import Prelude hiding (max)
import Generics.Deriving
import qualified Data.Set as S
import Data.List
import Control.Monad (liftM)

import qualified Test.QuickCheck as Q


-- | Entry point to generalize constructors.  We pass in a list of indexes from
-- value generalizations so we don't try to generalize those constructors (or
-- anything below).
constrsGen :: (SubTypes a, Generic a, ConNames (Rep a))
           => ScArgs -> a -> (a -> Q.Property) -> [Idx] -> IO [Idx]
constrsGen args d prop vs = do
  putStrLn ""
  smartPrtLn "Extrapolating Constructors ..."
  (_, idxs) <- iter' forest (Idx 0 0) []
  return idxs

  forest     = let forest' = mkSubstForest d True in
               -- This ensures we don't try to replace anything below the indexs
               -- from vs.  It does NOT ensure we don't replace equal indexes.
               foldl' (\f idx -> forestReplaceChildren f idx False) forest' vs

  iter'      = iter d test next prop (scMaxDepth args)

  -- Check if this has been generalized already during extrapolating values.
  test x idx = do res <- extrapolateConstrs args x idx prop
                  return $ idx `notElem` vs && res

  -- Control-flow.
  next _ res forest' idx idxs =
    iter' (if res then forestReplaceChildren forest' idx False else forest')
      idx { column = column idx + 1 } idxs'

    idxs' = if res then idx : idxs else idxs


-- | Return True if we can generalize; False otherwise.
extrapolateConstrs :: (SubTypes a, Generic a, ConNames (Rep a))
  => ScArgs -> a -> Idx -> (a -> Q.Property) -> IO Bool
extrapolateConstrs args a idx prop =
  recConstrs $ S.singleton $ subConstr a idx $ scMaxDepth args
  notProp = Q.expectFailure . prop
  allConstrs = S.fromList (conNames a)

  recConstrs :: S.Set String -> IO Bool
  recConstrs constrs =
    let newConstr x = subConstr x idx (scMaxDepth args) `S.insert` constrs in
    -- Check if every possible constructor is an element of constrs passed in.
    if allConstrs `S.isSubsetOf` constrs
      then return True
      else do v <- arbSubset args a idx notProp constrs
              case v of
                Result x      -> recConstrs (newConstr x)
                FailedPreCond -> return False
                FailedProp    -> return False
                BaseType      -> return False


-- | For a value a (used just for typing), and a list of representations of
-- constructors cs, arbSubset generages a new value b, if possible, such that b
-- has the same type as a, and b's constructor is not found in cs.
-- Assumes there is some new constructor to test with.
arbSubset :: (SubTypes a, Generic a, ConNames (Rep a))
          => ScArgs -> a -> Idx -> (a -> Q.Property)
          -> S.Set String -> IO (Result a)
arbSubset args a idx prop constrs =
  liftM snd $ iterateArbIdx a (idx, scMaxDepth args)
                (scMaxExists args) (scMaxSize args) prop'
  prop' b = newConstr b Q.==> prop b
  -- Make sure b's constructor is a new one.
  newConstr b = not $ subConstr b idx (scMaxDepth args) `S.member` constrs


-- | Get the constructor at an index in x.
subConstr :: SubTypes a => a -> Idx -> Maybe Int -> String
subConstr x idx max =
  case getAtIdx x idx max of
    Nothing -> errorMsg "constrs'"
    Just x' -> subTconstr x'

  subTconstr (SubT v) = toConstr v