-- |
-- Module      : Test.LeanCheck.Function.ShowFunction
-- Copyright   : (c) 2015-2018 Rudy Matela
-- License     : 3-Clause BSD  (see the file LICENSE)
-- Maintainer  : Rudy Matela <rudy@matela.com.br>
--
-- This module is part of LeanCheck,
-- a simple enumerative property-based testing library.
--
-- This module exports the 'ShowFunction' typeclass,
-- its instances and related functions.
--
-- Using this module, it is possible to implement
-- a Show instance for functions:
--
-- > import Test.LeanCheck.ShowFunction
-- > instance (Show a, Listable a, ShowFunction b) => Show (a->b) where
-- >   show = showFunction 8
--
-- This shows functions as a case pattern with up to 8 cases.
--
-- The module
-- @Test.LeanCheck.Function.Show@ ('Test.LeanCheck.Function.Show')
-- exports an instance like the one above.
module Test.LeanCheck.Function.ShowFunction
  ( showFunction
  , showFunctionLine
  , Binding
  , bindings
  , ShowFunction (..)
  , tBindingsShow
  -- * Re-exports
  , Listable
  )
where

import Test.LeanCheck.Core
import Test.LeanCheck.Error (errorToNothing)
import Test.LeanCheck.Utils.Types
import Data.List
import Data.Maybe

-- | A functional binding in a showable format.
type Binding = ([String], Maybe String)

-- | 'ShowFunction' values are those for which
--   we can return a list of functional bindings.
--
-- As a user, you probably want 'showFunction' and 'showFunctionLine'.
--
-- Non functional instances should be defined by:
--
-- > instance ShowFunction Ty where tBindings = tBindingsShow
class ShowFunction a where
  tBindings :: a -> [[Binding]]

-- | Given a 'ShowFunction' value, return a list of bindings
--   for printing.  Examples:
--
-- > bindings True == [([],True)]
-- > bindings (id::Int) == [(["0"],"0"), (["1"],"1"), (["-1"],"-1"), ...
-- > bindings (&&) == [ (["False","False"], "False")
-- >                  , (["False","True"], "False")
-- >                  , (["True","False"], "False")
-- >                  , (["True","True"], "True")
-- >                  ]
bindings :: ShowFunction a => a -> [Binding]
bindings = concat . tBindings


-- instances for (algebraic/numeric) data types --
-- | A default implementation of tBindings for already 'Show'-able types.
tBindingsShow :: Show a => a -> [[Binding]]
tBindingsShow x = [[([],errorToNothing $ show x)]]

instance ShowFunction ()       where tBindings = tBindingsShow
instance ShowFunction Bool     where tBindings = tBindingsShow
instance ShowFunction Int      where tBindings = tBindingsShow
instance ShowFunction Integer  where tBindings = tBindingsShow
instance ShowFunction Char     where tBindings = tBindingsShow
instance ShowFunction Float    where tBindings = tBindingsShow
instance ShowFunction Double   where tBindings = tBindingsShow
instance ShowFunction Ordering where tBindings = tBindingsShow
instance Show a => ShowFunction [a]       where tBindings = tBindingsShow
instance Show a => ShowFunction (Maybe a) where tBindings = tBindingsShow
instance (Show a, Show b) => ShowFunction (Either a b) where tBindings = tBindingsShow
instance (Show a, Show b) => ShowFunction (a,b) where tBindings = tBindingsShow

-- instance for functional value type --
instance (Show a, Listable a, ShowFunction b) => ShowFunction (a->b) where
  tBindings f = concatMapT tBindingsFor tiers
    where tBindingsFor x = mapFst (show x:) `mapT` tBindings (f x)
          mapFst f (x,y) = (f x, y)

paren :: String -> String
paren s = "(" ++ s ++ ")"

varnamesFor :: ShowFunction a => a -> [String]
varnamesFor = zipWith const varnames . fst . head . bindings
  where varnames = ["x","y","z","w"] ++ map (++"'") varnames

showTuple :: [String] -> String
showTuple [x] = x
showTuple xs  = paren $ intercalate "," xs

showNBindingsOf :: ShowFunction a => Int -> Int -> a -> [String]
showNBindingsOf m n f = take n bs
                     ++ ["..." | length bs' >= m || length bs > n]
  where bs' = take m $ bindings f
        bs = [ showTuple as ++ " -> " ++ r
             | (as, Just r) <- bs' ]

isValue :: ShowFunction a => a -> Bool
isValue f = case bindings f of
              [([],_)] -> True
              _        -> False

showValueOf :: ShowFunction a => a -> String
showValueOf x = case snd . head . bindings $ x of
                  Nothing -> "undefined"
                  Just x' -> x'

-- | Given a number of patterns to show, shows a 'ShowFunction' value.
--
-- > showFunction undefined True == "True"
-- > showFunction 3 (id::Int) == "\\x -> case x of\n\
-- >                              \        0 -> 0\n\
-- >                              \        1 -> 1\n\
-- >                              \        -1 -> -1\n\
-- >                              \        ...\n"
-- > showFunction 4 (&&) == "\\x y -> case (x,y) of\n\
-- >                         \          (False,False) -> False\n\
-- >                         \          (False,True) -> False\n\
-- >                         \          (True,False) -> False\n\
-- >                         \          (True,True) -> True\n"
--
-- This can be used as an implementation of show for functions:
--
-- > instance (Show a, Listable a, ShowFunction b) => Show (a->b) where
-- >   show = showFunction 8
showFunction :: ShowFunction a => Int -> a -> String
showFunction n = showFunctionL False (n*n+1) n

-- | Same as showFunction, but has no line breaks.
--
-- > showFunction 2 (id::Int) == "\\x -> case x of 0 -> 0; 1 -> 1; ..."
showFunctionLine :: ShowFunction a => Int -> a -> String
showFunctionLine n = showFunctionL True (n*n+1) n

-- | isUndefined checks if a function is totally undefined.
-- When it is not possible to check all values, it returns false
isUndefined :: ShowFunction a => Int -> a -> Bool
isUndefined m f = length bs < m && all (isNothing . snd) bs
  where bs = take m $ bindings f

-- The first boolean parameter tells if we are showing
-- the function on a single line
showFunctionL :: ShowFunction a => Bool -> Int -> Int -> a -> String
showFunctionL singleLine m n f | isValue f = showValueOf f
showFunctionL singleLine m n f | otherwise = lambdaPat ++ caseExp
  where
    vs = varnamesFor f
    lambdaPat = "\\" ++ unwords vs ++ " -> "
    casePat = "case " ++ showTuple vs ++ " of"
    bs = showNBindingsOf m n f
    sep | singleLine = " "
        | otherwise = "\n"
    cases | singleLine = intercalate "; " bs
          | otherwise  = unlines
                       $ (replicate (length lambdaPat + 2) ' ' ++) `map` bs
    caseExp = if isUndefined m f
                then "undefined"
                else casePat ++ sep ++ cases

-- instances for further tuple arities --
instance (Show a, Show b, Show c)
      => ShowFunction (a,b,c) where tBindings = tBindingsShow
instance (Show a, Show b, Show c, Show d)
      => ShowFunction (a,b,c,d) where tBindings = tBindingsShow
instance (Show a, Show b, Show c, Show d, Show e)
      => ShowFunction (a,b,c,d,e) where tBindings = tBindingsShow
instance (Show a, Show b, Show c, Show d, Show e, Show f)
      => ShowFunction (a,b,c,d,e,f) where tBindings = tBindingsShow
instance (Show a, Show b, Show c, Show d, Show e, Show f, Show g)
      => ShowFunction (a,b,c,d,e,f,g) where tBindings = tBindingsShow
instance (Show a, Show b, Show c, Show d, Show e, Show f, Show g, Show h)
      => ShowFunction (a,b,c,d,e,f,g,h) where tBindings = tBindingsShow

-- instance for types from Test.LeanCheck.Utils.Types
instance ShowFunction Nat   where tBindings = tBindingsShow
instance ShowFunction Nat1  where tBindings = tBindingsShow
instance ShowFunction Nat2  where tBindings = tBindingsShow
instance ShowFunction Nat3  where tBindings = tBindingsShow
instance ShowFunction Nat4  where tBindings = tBindingsShow
instance ShowFunction Nat5  where tBindings = tBindingsShow
instance ShowFunction Nat6  where tBindings = tBindingsShow
instance ShowFunction Nat7  where tBindings = tBindingsShow
instance ShowFunction Int1  where tBindings = tBindingsShow
instance ShowFunction Int2  where tBindings = tBindingsShow
instance ShowFunction Int3  where tBindings = tBindingsShow
instance ShowFunction Int4  where tBindings = tBindingsShow
instance ShowFunction Word1 where tBindings = tBindingsShow
instance ShowFunction Word2 where tBindings = tBindingsShow
instance ShowFunction Word3 where tBindings = tBindingsShow
instance ShowFunction Word4 where tBindings = tBindingsShow