{-# LANGUAGE TemplateHaskell, RecordWildCards, DeriveLift, TupleSections, CPP, TypeOperators #-}
module CollectDataTypes where

import Language.Haskell.TH
import Language.Haskell.TH.Syntax
import Language.Haskell.TH.Ppr
import Language.Haskell.Interpreter hiding (lift)
import Data.Maybe
import Data.List
import System.Process
import Data.List.Split
import Control.Monad
import Control.Monad.Trans.Maybe
import Control.Monad.IO.Class
import Text.Printf
import Data.Either
import Data.Char
import Data.Function
import Test.QuickCheck
import Test.QuickCheck.Function

data DataType =
  DataType {
    dt_package :: String,
    dt_module :: String,
    dt_type :: String }
  deriving (Show, Lift)

getPackageDataTypes :: String -> IO [DataType]
getPackageDataTypes pkg = do
  mods <- filter isValidModule <$> getPackageModules pkg
  typess <- mapM getModuleDataTypes mods
  return [DataType pkg mod typ | (mod, types) <- zip mods typess, typ <- types]

getPackageModules :: String -> IO [String]
getPackageModules pkg =
  concatMap (parseWords . words) . splitOn ", " . unwords . words <$> readProcess cmd args ""
  where
    cmd:args = ["ghc-pkg", "field", pkg, "exposed-modules", "--simple-output"]
    parseWords [mod, "from", _] = [mod]
    parseWords xs = xs

getModuleDataTypes :: String -> IO [String]
getModuleDataTypes mod = do
  putStrLn mod
  Right names <- runInterpreter $ getModuleExports mod
  return [x | Data x _ <- names]

haskellName :: DataType -> String
haskellName DataType{..} = printf "%s.%s" dt_module (stripParens dt_type)
  where
    stripParens = reverse . dropWhile (== ')') . reverse . dropWhile (== '(')

dataTypeType :: DataType -> Q (Maybe Type)
dataTypeType dt = do
  mname <- lookupTypeName (haskellName dt)
  case mname of
    Nothing -> return Nothing
    Just name -> Just <$> reifyType name

typeArity :: Type -> Maybe Int
typeArity (AppT (AppT ArrowT StarT) kind) = succ <$> typeArity kind
typeArity StarT = return 0
typeArity _ = Nothing

createProperties :: String -> Q [Dec]
createProperties pkg = do
  datatypes0 <- runIO (getPackageDataTypes pkg)
  let datatypes = [ dt | dt <- datatypes0, not $ haskellName dt `elem` typeBlacklist ]
  let mkImport dt = printf "import %s -- for %s" (dt_module dt) (dt_type dt)
  missingModules <- fmap (map mkImport . nubBy ((==) `on` dt_module)) $ filterM (\ dt -> isNothing <$> dataTypeType dt) datatypes
  unless (null missingModules) $ error ("Missing the following imports:\n" ++ unlines missingModules)
  namesAndDecs <- fmap concat $ mapM createProperty datatypes
  let (allNames, props) = unzip namesAndDecs
  allPropsDec <- [d| allProps =
                        $(pure $ ListE [ TupE [Just (LitE (StringL $ nameBase name)), Just (VarE name)]
                                              | name <- allNames ]
                         )
                 |]
  return $ allPropsDec ++ props

createProperty :: DataType -> Q [(Name, Dec)]
createProperty dt = do
  mtype <- dataTypeType dt
  -- TODO: monad?!
  case mtype of
    Nothing -> error $ "Can't find type in scope " ++ show dt
    Just typ -> case typeArity typ of
      Nothing -> pure []
      Just arity -> do
        Just name <- lookupTypeName (haskellName dt)
        Just int <- lookupTypeName "Int"
        nm <- newName ("prop_" ++ filter isAlphaNum (haskellName dt))
        nmCo <- newName ("prop_co_" ++ filter isAlphaNum (haskellName dt))
        nmFunction <- newName ("prop_function_" ++ filter isAlphaNum (haskellName dt))
        let propName = pure $ VarP nm
        let propNameCo = pure $ VarP nmCo
        let propNameFunction = pure $ VarP nmFunction
        let ty = pure $ foldl AppT (ConT name) $ replicate arity (ConT int)
        dArbitrary <- map (nm,) <$> [d| $propName = forAllBlind (arbitrary :: Gen $ty) (\ x -> x `seq` True) |]
        dCoArbitrary <- map (nmCo,) <$> [d| $propNameCo = forAllBlind (arbitrary :: Gen ($ty -> Integer)) (\ x -> x `seq` True) |]
        dFunction <- map (nmFunction,) <$> [d| $propNameFunction = forAllBlind (arbitrary :: Gen ($ty :-> Integer)) (\ x -> x `seq` True) |]
        return $ dArbitrary ++ dCoArbitrary ++ dFunction

typeBlacklist :: [String]
typeBlacklist = [ "Prelude.IO"
                , "Prelude.ReadS"
                , "Prelude.ShowS"
                , "System.IO.IO"
                , "System.IO.Error.IOError"
                , "Prelude.IOError"
                , "Data.Kind.Type"
                , "Data.Array.Byte.MutableByteArray"
                , "Data.IORef.IORef"
                , "Data.Kind.Constraint"
                , "Data.Unique.Unique"
                , "Data.STRef.STRef"
                , "Data.STRef.Lazy.STRef"
                , "Data.STRef.Strict.STRef"
                , "Data.Void.Void"
                , "Data.Proxy.KProxy"
                , "Data.Monoid.Endo"
                , "Data.Semigroup.Endo"
                , "Data.List.[]" -- This is buggy and annoying
                , "System.IO.HandlePosn"
                , "System.IO.Handle"
                , "Text.Printf.FieldFormatter" -- This is a function type and it
                                               -- requires an annoying coarbitrary instance
                , "Text.Printf.ModifierParser"
                , "Text.Show.ShowS"
                ] ++
                -- These are phantom types used for indexing
                [ "Data.Fixed.E" ++ show i | i <- [0,1,2,3,6,9,12] ] ++
#if MIN_VERSION_base(4,15,0)
                -- Exists but is deprecated
                [ "Data.Semigroup.Option" ] ++
#endif
                -- TODO: Some controversial ones?
                [ "System.IO.Error.IOErrorType" ] ++
                -- Some higher order types we ignore for the sake of CoArbitrary and Function issues
                [ "System.Console.GetOpt.OptDescr"
                , "System.Console.GetOpt.ArgOrder"
                , "System.Console.GetOpt.ArgDescr"
                ] ++
                -- System specific and, likewise, not easily doable in Function
                [ "System.IO.TextEncoding" ] ++
                -- Ignored for `Function` because the test monomorphises to `Int`. fixme.
                [ "Data.Complex.Complex" ]




modulePrefixBlacklist :: [String]
modulePrefixBlacklist = [ "GHC"
                        , "Foreign"
                        , "Control.Exception"
                          -- Exports things like MVar etc
                        , "Control.Concurrent"
                          -- Exports ST and RealWorld that we can't support
                        , "Control.Monad.ST"
                          -- Existential wrapper around a Typeable thing, could
                          -- be supported but would be a bit artificially
                          -- limited to wrapping a bunch of types we can list
                        , "Data.Dynamic"
                          -- We _could_ support this, but it would result in
                          -- the same problem as with Dynamic
                        , "Data.Typeable"
                        , "Type.Reflection"
                          -- System.Mem.Weak and System.Mem.Stable export
                          -- pointer types we don't support
                        , "System.Mem"
                          -- Exports an exception
                        , "System.Timeout"
                        -- Exports types, but not the constructors (or ways of
                        -- creating them, e.g. Number).  No feasible way to
                        -- create meaningful generator
                        , "Text.Read"
                        -- Old generics implementation that doesn't fit nicely
                        -- with arbitrary, as it has the same kind of problem
                        -- that Typeable and Dynamic face
                        , "Data.Data"
                        -- Platform specific types that one could implement but
                        -- would be tricky to keep consistent and correct
                        -- across a number of platforms, esp. since we don't
                        -- have good CI tests for them
                        , "System.Posix"
                        -- This exports a bunch of combinators whose only real
                        -- role is to build a parser of an opaque type,
                        -- consequently there isn't some super-interesting
                        -- useful work you can do with it and it should be OK
                        -- not to provide instances for it
                        , "Text.ParserCombinators.ReadP"
                        -- Slightly controversial, but this is only ignored for
                        -- the sake of CoArbitrary and Function
                        , "Data.Functor.Contravariant"
                        ]

isValidModule :: String -> Bool
isValidModule mod = not $ any (`isPrefixOf` mod) modulePrefixBlacklist
