-- | Functions for accessing the values of enumerations including 
-- compatibility with the property based testing frameworks QuickCheck and
-- SmallCheck.
module Test.Feat.Access(
  -- ** Accessing functions
  index,
  values,
  striped,
  bounded,
  
  -- ** A simple property tester
  ioFeat,
  ioAll,
  ioBounded,
  
  -- ** Compatibility
  -- *** QuickCheck
  uniform,
  -- *** SmallCheck
  toSeries,
  
  -- ** Non-class versions of the access functions
  valuesWith,
  stripedWith,
  boundedWith,
  uniformWith,
  toSeriesWith
  )where

-- testing-feat
import Test.Feat.Enumerate 
import Test.Feat.Class
-- base
import Data.List
-- quickcheck
import Test.QuickCheck
-- smallcheck
-- import Test.SmallCheck.Series -- Not needed

group :: Enumerate a -> Part -> Index -> Integer
group e p i = sum (map (card e) [0..p-1]) + i

split :: Enumerate a -> Integer -> (Part, Index)
split e i0 = go i0 0 where
  go i p = let crd = card e p in 
     if i < crd then (p,i)
     else go (i-crd) (p+1)

-- | Mainly as a proof of concept we can use the isomorphism between 
-- natural numbers and @(Part,Index)@ pairs to index into a type.
-- May not terminate for finite types.
-- Might be slow the first time it is used with a specific enumeration 
-- because cardinalities need to be calculated.
-- The computational complexity (after cardinalities are computed) is a polynomial
-- in the size of the resulting value.
index :: Enumerate a -> Integer -> a 
index e = uncurry (select e) . split e

-- | All values of the enumeration by increasing cost (which is the number
-- of constructors for most types). Also contains the cardinality of each list.
values :: Enumerable a => [(Integer,[a])]
values = valuesWith optimised

-- | A generalisation of @values@ that enumerates every nth value of the 
-- enumeration from a given starting point.
-- As a special case @values = striped 0 0 1@.
striped ::  Enumerable a => Part -> Index -> Integer -> [(Integer,[a])]
striped = stripedWith optimised 

-- | A version of vales that has a limited number of values in each inner list.
-- If the list corresponds to a Part which is larger than the bound it evenly
-- intersperses the values across the enumeration of the Part.
bounded :: Enumerable a => Integer -> [(Integer,[a])]
bounded = boundedWith optimised

-- | A rather simple but general property testing driver.
-- The property is an (funcurried) IO function that both tests and reports the 
-- error. The driver goes on forever or until the list is exhausted, 
-- reporting the coverage and the number of
-- tests before each new part.
ioFeat :: [(Integer,[a])] -> (a -> IO ()) -> IO ()
ioFeat vs f = go vs 0 0 where
  go ((c,xs):xss) s tot = do
    putStrLn $ "--- Testing "++show c++" values at size " ++ show s
    mapM f xs
    go xss (s+1) (tot + c)
  go []           s tot = putStrLn $ "--- Done. Tested "++ show tot++" values"

-- | Defined as @ioAll = 'ioFeat' 'values' @
ioAll :: Enumerable a => (a -> IO ()) -> IO ()
ioAll = ioFeat values

-- | Defined as @ioBounded n = 'ioFeat' ('bounded' n)@
ioBounded :: Enumerable a => Integer -> (a -> IO ()) -> IO ()
ioBounded n = ioFeat (bounded n)



-- | Compatibility with QuickCheck. Distribution is uniform generator over 
-- values bounded by the given size. Typical use: @sized uniform@.
uniform :: Enumerable a => Int -> Gen a
uniform = uniformWith optimised

-- | Compatibility with SmallCheck. 
toSeries :: Enumerable a => Int -> [a] 
toSeries = toSeriesWith optimised

-- | Non class version of 'values'.
valuesWith :: Enumerate a -> [(Integer,[a])]
valuesWith e = 
  [(crd,[select e p i|i <- [0..crd - 1]]) 
    |p <- [0..], let crd = card e p]

-- | Non class version of 'striped'.
stripedWith :: Enumerate a -> Part -> Index -> Integer -> [(Integer,[a])]
stripedWith e p o step = if space <= 0 
  then (0,[]) : stripedWith e (p+1) (o - crd) step
  else (d,thisP) : stripedWith e (p+1) (step-m-1) step
  where
    thisP = 
      [select e p x|x <- genericTake d $ iterate (+step) o]
    space = crd - o
    (d,m) = divMod space step
    crd = card e p

-- | Non class version of 'bounded'.
boundedWith :: Enumerate a -> Integer -> [(Integer,[a])]
boundedWith e n = map (samplePart e n) [0..]

samplePart :: Enumerate a -> Index -> Part -> (Integer,[a])
samplePart e m p = 
  let
    top   =  toRational $ (card e p) - 1
    step  =  top / toRational (m-1) 
    crd = card e p
  in if toRational m >= top 
       then (crd, map (select e p) [0..crd - 1])
       else let d = floor ((toRational crd)/ step) in 
         (d+1,[select e p (round (k * step))|k <- map toRational [0..d]])

-- | Non class version of 'uniform'.
uniformWith :: Enumerate a -> Part -> Gen a
uniformWith e maxp = let
  cards  = [(card e x, x) | x <- [maxp, maxp-1 .. 0]]
  tot    = sum $ fst $ unzip cards
  in if tot == 0 then uniformWith e (maxp+1) else do
    i <- choose (0,tot-1)
    return $ uncurry (select e) (lu i cards)
  where
    lu i ((crd,p):xs)  = if i<crd 
      then (p,i) 
      else lu (i-crd) xs
      
-- | Non class version of 'toSeries'.
toSeriesWith :: Enumerate a -> Int -> [a]
toSeriesWith e d = concat (take (d+1) $ map snd $ valuesWith e)