{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE PatternGuards #-}

-- TODO: suggest the convenience functions be put into Hint proper?
module Mueval.Interpreter where

import qualified Control.Exception.Extensible as E (SomeException (..), catch, evaluate)
import Control.Monad (forM_, guard, mplus, unless, when)
import Control.Monad.Trans (MonadIO)
import Control.Monad.Writer (runWriterT, tell)
import Data.Char (isDigit)

import System.Directory

import System.Exit (exitFailure)
import System.FilePath.Posix (takeBaseName)
import System.IO (openTempFile)

import Data.List
import Data.Monoid (Any (..))

import Language.Haskell.Interpreter (
    Extension (UnknownExtension),
    GhcError (..),
    Interpreter,
    InterpreterError (..),
    OptionVal (..),
    availableExtensions,
    eval,
    installedModulesInScope,
    languageExtensions,
    liftIO,
    loadModules,
    reset,
    runInterpreter,
    set,
    setImportsQ,
    setTopLevelModules,
    typeOf,
 )
import Language.Haskell.Interpreter.Unsafe (unsafeSetGhcOption)

import Mueval.ArgsParse (Options (..))
import qualified Mueval.Context as MC (qualifiedModules)
import qualified Mueval.Resources as MR (limitResources)

readExt :: String -> Extension
readExt s = case reads s of
    [(e, [])] -> e
    _ -> UnknownExtension s

{- | The actual calling of Hint functionality. The heart of this just calls
   'eval', but we do so much more - we disable Haskell extensions,
   hide all packages, make sure one cannot call unimported
   functions, typecheck, set resource limits for this
   thread, and do some error handling.
-}
interpreter :: Options -> Interpreter (String, String, String)
interpreter
    Options
        { extensions = exts
        , namedExtensions = nexts
        , rLimits = rlimits
        , typeOnly = noEval
        , loadFile = load
        , expression = expr
        , packageTrust = trust
        , trustedPackages = trustPkgs
        , modules = m
        } = do
        let lexts = (guard exts >> glasgowExtensions) ++ map readExt nexts
        -- Explicitly adding ImplicitPrelude because of
        -- http://darcsden.com/jcpetruzza/hint/issue/1
        unless (null lexts) $ set [languageExtensions := (UnknownExtension "ImplicitPrelude" : lexts)]
        when trust $ do
            unsafeSetGhcOption "-fpackage-trust"
            forM_ (trustPkgs >>= words) $ \pkg ->
                unsafeSetGhcOption ("-trust " ++ pkg)

        reset -- Make sure nothing is available
        set [installedModulesInScope := False]

        -- if we're given a file of definitions, we need to first copy it to a temporary file in /tmp (cpload),
        -- then tell Hint to parse/read it, then extract the 'module name' of the file,
        -- and tell Hint to expose the module into memory; then we need to store the temporary file's filepath
        -- so we can try to clean up after ourselves later.
        lfl' <-
            if (load /= "")
                then
                    ( do
                        lfl <- liftIO (cpload load)
                        loadModules [lfl]
                        -- We need to mangle the String to
                        -- turn a filename into a module.
                        setTopLevelModules [takeBaseName load]
                        return lfl
                    )
                else (return "")

        liftIO $ MR.limitResources rlimits

        case m of
            Nothing -> return ()
            Just ms -> do
                let unqualModules = zip ms (repeat Nothing)
                setImportsQ (unqualModules ++ MC.qualifiedModules)

        -- clean up our tmp file here; must be *after* setImportsQ
        when (load /= "") $ liftIO (removeFile lfl')

        -- we don't deliberately don't check if the expression typechecks
        -- this way we get an "InterpreterError" we can display
        etype <- typeOf expr
        result <-
            if noEval
                then return ""
                else eval expr

        return (expr, etype, result)

{- | Wrapper around 'interpreter'; supplies a fresh GHC API session and
 error-handling. The arguments are largely passed on, and the results lightly parsed.
-}
interpreterSession :: Options -> IO ()
interpreterSession opts = do
    r <- runInterpreter (interpreter opts)
    case r of
        Left err -> printInterpreterError err
        Right (e, et, val) -> do
            when
                (printType opts)
                (sayIO e >> sayIOOneLine et)
            sayIO val
  where
    sayIOOneLine = sayIO . unwords . words

-- | Given a filepath (containing function definitions), copy it to a temporary file and change directory to it, returning the new filepath.
cpload :: FilePath -> IO FilePath
cpload definitions = do
    tmpdir <- getTemporaryDirectory
    (tempfile, _) <- System.IO.openTempFile tmpdir "mueval.hs"
    liftIO $ copyFile definitions tempfile
    setCurrentDirectory tmpdir -- will at least mess up relative links
    return tempfile

---------------------------------
-- Handling and outputting results
-- TODO: this whole section is a hack

{- | Print the String (presumably the result
 of interpreting something), but only print the first 1024 characters to avoid
 flooding. Lambdabot has a similar limit.
-}
sayIO :: String -> IO ()
sayIO str = do
    (out, b) <- render 1024 str
    putStrLn out
    when b exitFailure

{- | Oh no, something has gone wrong. If it's a compilation error pretty print
 the first 1024 chars of it and throw an "ExitException"
 otherwise rethrow the exception in String form.
-}
printInterpreterError :: InterpreterError -> IO ()
printInterpreterError (WontCompile errors) =
    -- if we get a compilation error we print it directly to avoid \"mueval: ...\"
    -- maybe it should go to stderr?
    do
        sayIO $ concatMap (dropLinePosition . errMsg) errors
        exitFailure
  where
    -- each error starts with the line position, which is uninteresting
    dropLinePosition e
        | Just s <- parseErr e = s
        | otherwise = e -- if the parse fails we fallback on printing the whole error
    parseErr e = do
        s <- stripPrefix "<interactive>:" e
        skipSpaces =<< (skipNumber =<< skipNumber s)
    skip x (y : xs)
        | x == y = Just xs
        | otherwise = Nothing
    skip _ _ = Nothing
    skipNumber = skip ':' . dropWhile isDigit
    skipSpaces xs =
        let xs' = dropWhile (== ' ') xs
         in skip '\n' xs' `mplus` return xs'

-- other exceptions indicate some problem in Mueval or the environment,
-- so we rethrow them for debugging purposes
printInterpreterError other = error (show other)

-- Constant
exceptionMsg :: String
exceptionMsg = "*Exception: "

-- | Renders the input String including its exceptions using @exceptionMsg@
render ::
    (Control.Monad.Trans.MonadIO m, Functor m) =>
    -- | max number of characters to include
    Int ->
    -- | input
    String ->
    -- | ( output, @True@ if we found an exception )
    m (String, Bool)
render i xs =
    do
        (out, Any b) <- runWriterT $ render' i (toStream xs)
        return (out, b)
  where
    render' n _ | n <= 0 = return ""
    render' n s = render'' n =<< liftIO s

    render'' _ End = return ""
    render'' n (Cons x s) = fmap (x :) $ render' (n - 1) s
    render'' n (Exception s) = do
        tell (Any True)
        fmap (take n exceptionMsg ++) $ render' (n - length exceptionMsg) s

data Stream = Cons Char (IO Stream) | Exception (IO Stream) | End

toStream :: String -> IO Stream
toStream str =
    E.evaluate (uncons str)
        `E.catch` \(E.SomeException e) -> return . Exception . toStream . show $ e
  where
    uncons [] = End
    uncons (x : xs) = x `seq` Cons x (toStream xs)

-- Copied from old hint, removed from hint since 0.5.0.
glasgowExtensions :: [Extension]
glasgowExtensions = intersect availableExtensions exts612 -- works also for 608 and 610
  where
    exts612 =
        map
            readExt
            [ "PrintExplicitForalls"
            , "ForeignFunctionInterface"
            , "UnliftedFFITypes"
            , "GADTs"
            , "ImplicitParams"
            , "ScopedTypeVariables"
            , "UnboxedTuples"
            , "TypeSynonymInstances"
            , "StandaloneDeriving"
            , "DeriveDataTypeable"
            , "FlexibleContexts"
            , "FlexibleInstances"
            , "ConstrainedClassMethods"
            , "MultiParamTypeClasses"
            , "FunctionalDependencies"
            , "MagicHash"
            , "PolymorphicComponents"
            , "ExistentialQuantification"
            , "UnicodeSyntax"
            , "PostfixOperators"
            , "PatternGuards"
            , "LiberalTypeSynonyms"
            , "ExplicitForAll"
            , "RankNTypes"
            , "ImpredicativeTypes"
            , "TypeOperators"
            , "RecursiveDo"
            , "DoRec"
            , "ParallelListComp"
            , "EmptyDataDecls"
            , "KindSignatures"
            , "GeneralizedNewtypeDeriving"
            , "TypeFamilies"
            ]
