
module Agda.Compiler.MAlonzo.Primitives where

import Control.Arrow ( second )
import Control.Monad.Trans.Maybe ( MaybeT(MaybeT, runMaybeT) )

import qualified Data.List as List
import           Data.Map (Map)
import qualified Data.Map as Map
import qualified Data.Set as Set
import qualified Data.HashMap.Strict as HMap
import Data.Maybe
import qualified Data.Text as T

import Agda.Compiler.Common
import Agda.Compiler.ToTreeless
import Agda.Compiler.MAlonzo.Misc
import Agda.Compiler.MAlonzo.Pretty
import Agda.Syntax.Common
import Agda.Syntax.Internal
import Agda.Syntax.Treeless
import Agda.TypeChecking.Monad
import Agda.TypeChecking.Primitive
import Agda.TypeChecking.Reduce
import Agda.TypeChecking.Pretty

import Agda.Utils.Either
import Agda.Utils.Lens
import Agda.Utils.Pretty (prettyShow)
import qualified Agda.Utils.Haskell.Syntax as HS

import Agda.Utils.Impossible

newtype MainFunctionDef = MainFunctionDef Definition

data CheckedMainFunctionDef = CheckedMainFunctionDef
  { checkedMainDef :: MainFunctionDef
  , checkedMainDecl :: HS.Decl
  }

-- Andreas, 2019-04-29, issue #3731: exclude certain kinds of names, like constructors.
-- TODO: Also only consider top-level definition (not buried inside a module).
asMainFunctionDef :: Definition -> Maybe MainFunctionDef
asMainFunctionDef d = case (theDef d) of
    Axiom{}                             -> perhaps
    Function{ funProjection = Nothing } -> perhaps
    Function{ funProjection = Just{}  } -> no
    AbstractDefn{}                      -> no
    GeneralizableVar{}                  -> no
    DataOrRecSig{}                      -> no
    Datatype{}                          -> no
    Record{}                            -> no
    Constructor{}                       -> no
    Primitive{}                         -> no
    PrimitiveSort{}                     -> no
  where
  isNamedMain = "main" == prettyShow (nameConcrete . qnameName . defName $ d)  -- ignores the qualification!?
  perhaps | isNamedMain = Just $ MainFunctionDef d
          | otherwise   = no
  no                    = Nothing

mainFunctionDefs :: Interface -> [MainFunctionDef]
mainFunctionDefs i = catMaybes $ asMainFunctionDef <$> defs
  where
    defs = HMap.elems $ iSignature i ^. sigDefinitions

-- | Check that the main function has type IO a, for some a.
checkTypeOfMain :: Definition -> HsCompileM (Maybe CheckedMainFunctionDef)
checkTypeOfMain def = runMaybeT $ do
  -- Only indicate main functions in the main module.
  isMainModule <- curIsMainModule
  mainDef <- MaybeT $ pure $ if isMainModule then asMainFunctionDef def else Nothing
  liftTCM $ checkTypeOfMain' mainDef

checkTypeOfMain' :: MainFunctionDef -> TCM CheckedMainFunctionDef
checkTypeOfMain' m@(MainFunctionDef def) = CheckedMainFunctionDef m <$> do
    Def io _ <- primIO
    ty <- reduce $ defType def
    case unEl ty of
      Def d _ | d == io -> return mainAlias
      _                 -> do
        err <- fsep $
          pwords "The type of main should be" ++
          [prettyTCM io] ++ pwords " A, for some A. The given type is" ++ [prettyTCM ty]
        typeError $ GenericError $ show err
  where
    mainAlias = HS.FunBind [HS.Match mainLHS [] mainRHS emptyBinds ]
    mainLHS   = HS.Ident "main"
    mainRHS   = HS.UnGuardedRhs $ HS.App mazCoerce (HS.Var $ HS.UnQual $ dname $ defName def)

treelessPrimName :: TPrim -> String
treelessPrimName p =
  case p of
    PQuot   -> "quotInt"
    PRem    -> "remInt"
    PSub    -> "subInt"
    PAdd    -> "addInt"
    PMul    -> "mulInt"
    PGeq    -> "geqInt"
    PLt     -> "ltInt"
    PEqI    -> "eqInt"
    PQuot64 -> "quot64"
    PRem64  -> "rem64"
    PSub64  -> "sub64"
    PAdd64  -> "add64"
    PMul64  -> "mul64"
    PLt64   -> "lt64"
    PEq64   -> "eq64"
    PITo64  -> "word64FromNat"
    P64ToI  -> "word64ToNat"
    PEqF    -> "MAlonzo.RTE.Float.doubleDenotEq"
    -- MAlonzo uses literal patterns, so we don't need equality for the other primitive types
    PEqC    -> __IMPOSSIBLE__
    PEqS    -> __IMPOSSIBLE__
    PEqQ    -> __IMPOSSIBLE__
    PSeq    -> "seq"
    -- primitives only used by GuardsToPrims transformation, which MAlonzo doesn't use
    PIf     -> __IMPOSSIBLE__

-- | Haskell modules to be imported for BUILT-INs
importsForPrim :: BuiltinThings PrimFun -> [Definition] -> [HS.ModuleName]
importsForPrim builtinThings defs = xForPrim table builtinThings defs ++ [HS.ModuleName "Data.Text"]
  where
  table = Map.fromDistinctAscList $ map (second HS.ModuleName)
    -- KEEP THIS LIST IN ALPHABETICAL ORDER!
    -- Otherwise, Map.fromDistinctAscList will produce garbage.
    [ "CHAR"                       |-> "Data.Char"
    , "primFloatCeiling"           |-> "MAlonzo.RTE.Float"
    , "primFloatDecode"            |-> "MAlonzo.RTE.Float"
    , "primFloatEncode"            |-> "MAlonzo.RTE.Float"
    , "primFloatEquality"          |-> "MAlonzo.RTE.Float"
    , "primFloatFloor"             |-> "MAlonzo.RTE.Float"
    , "primFloatInequality"        |-> "MAlonzo.RTE.Float"
    , "primFloatIsSafeInteger"     |-> "MAlonzo.RTE.Float"
    , "primFloatLess"              |-> "MAlonzo.RTE.Float"
    , "primFloatRound"             |-> "MAlonzo.RTE.Float"
    , "primFloatToRatio"           |-> "MAlonzo.RTE.Float"
    , "primFloatToWord64"          |-> "MAlonzo.RTE.Float"
    , "primIsAlpha"                |-> "Data.Char"
    , "primIsAscii"                |-> "Data.Char"
    , "primIsDigit"                |-> "Data.Char"
    , "primIsHexDigit"             |-> "Data.Char"
    , "primIsLatin1"               |-> "Data.Char"
    , "primIsLower"                |-> "Data.Char"
    , "primIsPrint"                |-> "Data.Char"
    , "primIsSpace"                |-> "Data.Char"
    , "primRatioToFloat"           |-> "MAlonzo.RTE.Float"
    , "primToLower"                |-> "Data.Char"
    , "primToUpper"                |-> "Data.Char"
    ]
  (|->) = (,)

--------------

xForPrim :: Map String a -> BuiltinThings PrimFun -> [Definition] -> [a]
xForPrim table builtinThings defs = catMaybes
    [ Map.lookup s table
    | (s, def) <- Map.toList builtinThings
    , getName def `Set.member` qs
    ]
  where
  qs = Set.fromList $ map defName defs
  getName (Builtin t)            = getPrimName t
  getName (Prim (PrimFun q _ _)) = q


-- | Definition bodies for primitive functions
primBody :: MonadTCError m => String -> m HS.Exp
primBody s = maybe unimplemented (fromRight (hsVarUQ . HS.Ident) <$>) $
             List.lookup s $
  [
  -- Integer functions
    "primIntegerPlus"    |-> binAsis "(+)" "Integer"
  , "primIntegerMinus"   |-> binAsis "(-)" "Integer"
  , "primIntegerTimes"   |-> binAsis "(*)" "Integer"
  , "primIntegerDiv"     |-> binAsis "div" "Integer"
  , "primIntegerMod"     |-> binAsis "mod" "Integer"
  , "primIntegerEquality"|-> rel "(==)" "Integer"
  , "primIntegerLess"    |-> rel "(<)"  "Integer"
  , "primIntegerAbs"     |-> return "(abs :: Integer -> Integer)"
  , "primNatToInteger"   |-> return "(id :: Integer -> Integer)"
  , "primShowInteger"    |-> return "(Data.Text.pack . show :: Integer -> Data.Text.Text)"

  -- Levels
  , "primLevelZero"   |-> return "()"
  , "primLevelSuc"    |-> return "(\\ _ -> ())"
  , "primLevelMax"    |-> return "(\\ _ _ -> ())"

  -- Sorts
  , "primSetOmega"    |-> return "()"
  , "primStrictSet"   |-> return "\\ _ -> ()"

  -- Natural number functions
  , "primNatPlus"      |-> binNat "(+)"
  , "primNatMinus"     |-> binNat "(\\ x y -> max 0 (x - y))"
  , "primNatTimes"     |-> binNat "(*)"
  , "primNatDivSucAux" |-> binNat4 "(\\ k m n j -> k + div (max 0 $ n + m - j) (m + 1))"
  , "primNatModSucAux" |-> binNat4 "(\\ k m n j -> if n > j then mod (n - j - 1) (m + 1) else (k + n))"
  , "primNatEquality"  |-> relNat "(==)"
  , "primNatLess"      |-> relNat "(<)"
  , "primShowNat"      |-> return "(Data.Text.pack . show :: Integer -> Data.Text.Text)"

  -- Machine word functions
  , "primWord64ToNat"   |-> return "MAlonzo.RTE.word64ToNat"
  , "primWord64FromNat" |-> return "MAlonzo.RTE.word64FromNat"
  , "primWord64ToNatInjective" |-> return mazErasedName

  -- Floating point functions
  , "primFloatEquality"          |-> return "MAlonzo.RTE.Float.doubleEq"
  , "primFloatInequality"        |-> return "MAlonzo.RTE.Float.doubleLe"
  , "primFloatLess"              |-> return "MAlonzo.RTE.Float.doubleLt"
  , "primFloatIsInfinite"        |-> return "(isInfinite :: Double -> Bool)"
  , "primFloatIsNaN"             |-> return "(isNaN :: Double -> Bool)"
  , "primFloatIsNegativeZero"    |-> return "(isNegativeZero :: Double -> Bool)"
  , "primFloatIsSafeInteger"     |-> return "MAlonzo.RTE.Float.isSafeInteger"
  , "primFloatToWord64"          |-> return "MAlonzo.RTE.Float.doubleToWord64"
  , "primFloatToWord64Injective" |-> return mazErasedName
  , "primNatToFloat"             |-> return "(MAlonzo.RTE.Float.intToDouble :: Integer -> Double)"
  , "primIntToFloat"             |-> return "(MAlonzo.RTE.Float.intToDouble :: Integer -> Double)"
  , "primFloatRound"             |-> return "MAlonzo.RTE.Float.doubleRound"
  , "primFloatFloor"             |-> return "MAlonzo.RTE.Float.doubleFloor"
  , "primFloatCeiling"           |-> return "MAlonzo.RTE.Float.doubleCeiling"
  , "primFloatToRatio"           |-> return "MAlonzo.RTE.Float.doubleToRatio"
  , "primRatioToFloat"           |-> return "MAlonzo.RTE.Float.ratioToDouble"
  , "primFloatDecode"            |-> return "MAlonzo.RTE.Float.doubleDecode"
  , "primFloatEncode"            |-> return "MAlonzo.RTE.Float.doubleEncode"
  , "primShowFloat"              |-> return "(Data.Text.pack . show :: Double -> Data.Text.Text)"
  , "primFloatPlus"              |-> return "MAlonzo.RTE.Float.doublePlus"
  , "primFloatMinus"             |-> return "MAlonzo.RTE.Float.doubleMinus"
  , "primFloatTimes"             |-> return "MAlonzo.RTE.Float.doubleTimes"
  , "primFloatNegate"            |-> return "MAlonzo.RTE.Float.doubleNegate"
  , "primFloatDiv"               |-> return "MAlonzo.RTE.Float.doubleDiv"
  , "primFloatPow"               |-> return "MAlonzo.RTE.Float.doublePow"
  , "primFloatSqrt"              |-> return "MAlonzo.RTE.Float.doubleSqrt"
  , "primFloatExp"               |-> return "MAlonzo.RTE.Float.doubleExp"
  , "primFloatLog"               |-> return "MAlonzo.RTE.Float.doubleLog"
  , "primFloatSin"               |-> return "MAlonzo.RTE.Float.doubleSin"
  , "primFloatCos"               |-> return "MAlonzo.RTE.Float.doubleCos"
  , "primFloatTan"               |-> return "MAlonzo.RTE.Float.doubleTan"
  , "primFloatASin"              |-> return "MAlonzo.RTE.Float.doubleASin"
  , "primFloatACos"              |-> return "MAlonzo.RTE.Float.doubleACos"
  , "primFloatATan"              |-> return "MAlonzo.RTE.Float.doubleATan"
  , "primFloatATan2"             |-> return "MAlonzo.RTE.Float.doubleATan2"
  , "primFloatSinh"              |-> return "MAlonzo.RTE.Float.doubleSinh"
  , "primFloatCosh"              |-> return "MAlonzo.RTE.Float.doubleCosh"
  , "primFloatTanh"              |-> return "MAlonzo.RTE.Float.doubleTanh"
  , "primFloatASinh"             |-> return "MAlonzo.RTE.Float.doubleASinh"
  , "primFloatACosh"             |-> return "MAlonzo.RTE.Float.doubleACosh"
  , "primFloatATanh"             |-> return "MAlonzo.RTE.Float.doubleATanh"

  -- Character functions
  , "primCharEquality"   |-> rel "(==)" "Char"
  , "primIsLower"        |-> return "Data.Char.isLower"
  , "primIsDigit"        |-> return "Data.Char.isDigit"
  , "primIsAlpha"        |-> return "Data.Char.isAlpha"
  , "primIsSpace"        |-> return "Data.Char.isSpace"
  , "primIsAscii"        |-> return "Data.Char.isAscii"
  , "primIsLatin1"       |-> return "Data.Char.isLatin1"
  , "primIsPrint"        |-> return "Data.Char.isPrint"
  , "primIsHexDigit"     |-> return "Data.Char.isHexDigit"
  , "primToUpper"        |-> return "Data.Char.toUpper"
  , "primToLower"        |-> return "Data.Char.toLower"
  , "primCharToNat" |-> return "(fromIntegral . fromEnum :: Char -> Integer)"
  , "primNatToChar" |-> return "MAlonzo.RTE.natToChar"
  , "primShowChar"  |-> return "(Data.Text.pack . show :: Char -> Data.Text.Text)"
  , "primCharToNatInjective" |-> return mazErasedName

  -- String functions
  , "primStringUncons"   |-> return "Data.Text.uncons"
  , "primStringToList"   |-> return "Data.Text.unpack"
  , "primStringFromList" |-> return "Data.Text.pack"
  , "primStringAppend"   |-> binAsis "Data.Text.append" "Data.Text.Text"
  , "primStringEquality" |-> rel "(==)" "Data.Text.Text"
  , "primShowString"     |-> return "(Data.Text.pack . show :: Data.Text.Text -> Data.Text.Text)"
  , "primStringToListInjective" |-> return mazErasedName
  , "primStringFromListInjective" |-> return mazErasedName

  -- Reflection
  , "primQNameEquality"   |-> rel "(==)" "MAlonzo.RTE.QName"
  , "primQNameLess"       |-> rel "(<)" "MAlonzo.RTE.QName"
  , "primShowQName"       |-> return "Data.Text.pack . MAlonzo.RTE.qnameString"
  , "primQNameFixity"     |-> return "MAlonzo.RTE.qnameFixity"
  , "primQNameToWord64s"  |-> return "\\ qn -> (MAlonzo.RTE.nameId qn, MAlonzo.RTE.moduleId qn)"
  , "primQNameToWord64sInjective" |-> return mazErasedName
  , "primMetaEquality"    |-> rel "(==)" "Integer"
  , "primMetaLess"        |-> rel "(<)" "Integer"
  , "primShowMeta"        |-> return "\\ x -> Data.Text.pack (\"_\" ++ show (x :: Integer))"
  , "primMetaToNat"       |-> return "(id :: Integer -> Integer)"
  , "primMetaToNatInjective" |-> return mazErasedName

  -- Seq
  , "primForce"      |-> return "\\ _ _ _ _ x f -> f $! x"
  , "primForceLemma" |-> return mazErasedName

  -- Erase
  , "primEraseEquality" |-> return mazErasedName

  -- Cubical
  , builtinIMin       |-> return "(&&)"
  , builtinIMax       |-> return "(||)"
  , builtinINeg       |-> return "not"
  , "primPartial"     |-> return "\\_ _ x -> x"
  , "primPartialP"    |-> return "\\_ _ x -> x"
  , builtinPOr        |-> return "\\_ i _ _ x y -> if i then x else y"
  , builtinComp       |-> return "\\_ _ _ _ x -> x"
  , builtinTrans      |-> return "\\_ _ _ x -> x"
  , builtinHComp      |-> return "\\_ _ _ _ x -> x"
  , builtinSubOut     |-> return "\\_ _ _ _ x -> x"
  , builtin_glueU     |-> return "\\_ _ _ _ _ x -> x"
  , builtin_unglueU   |-> return "\\_ _ _ _ x -> x"
  , builtinFaceForall |-> return
                            "\\f -> f True == True && f False == True"
  , "primDepIMin"     |-> return "\\i f -> if i then f () else False"
  , "primIdFace"      |-> return "\\_ _ _ _ -> fst"
  , "primIdPath"      |-> return "\\_ _ _ _ -> snd"
  , "primIdJ"         |-> return "\\_ _ _ _ _ x _ _ -> x"
  , builtinIdElim     |-> return
                            "\\_ _ _ _ _ f x y -> f (fst y) x (snd y)"
  ]
  where
  x |-> s = (x, Left <$> s)
  binNat  op = return $ repl [op] "(<<0>> :: Integer -> Integer -> Integer)"
  binNat4 op = return $ repl [op] "(<<0>> :: Integer -> Integer -> Integer -> Integer -> Integer)"
  binAsis op ty = return $ repl [op, opty ty] $ "((<<0>>) :: <<1>>)"
  rel' toTy op ty = do
    return $ repl [op, ty, toTy] $
      "(\\ x y -> (<<0>> :: <<1>> -> <<1>> -> Bool) (<<2>> x) (<<2>> y))"
  relNat op = do
    return $ repl [op] $
      "(<<0>> :: Integer -> Integer -> Bool)"
  rel op ty  = rel' "" op ty
  opty t = t ++ "->" ++ t ++ "->" ++ t
  unimplemented = typeError $ NotImplemented s

  hLam x t = Lam (setHiding Hidden defaultArgInfo) (Abs x t)
  nLam x t = Lam (setHiding NotHidden defaultArgInfo) (Abs x t)

noCheckCover :: (HasBuiltins m, MonadReduce m) => QName -> m Bool
noCheckCover q = (||) <$> isBuiltin q builtinNat <*> isBuiltin q builtinInteger

----------------------

bltQual' :: String -> String -> HsCompileM String
bltQual' b s = prettyPrint <$> bltQual b s
