{-# LANGUAGE ExtendedDefaultRules #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TupleSections #-}
{-# OPTIONS_GHC -fno-warn-type-defaults #-}

-- | Run a IDE configured with the user's project(s).

module Stack.Ide (ide) where

import           Control.Concurrent
import           Control.Monad.Catch
import           Control.Monad.IO.Class
import           Control.Monad.Logger
import           Control.Monad.Reader
import           Data.Aeson
import qualified Data.ByteString as S
import qualified Data.ByteString.Char8 as S8
import           Data.ByteString.Lazy (ByteString)
import qualified Data.ByteString.Lazy as L
import           Data.List
import qualified Data.Map.Strict as M
import           Data.Maybe
import           Data.Monoid
import qualified Data.Set as S
import           Data.Text (Text)
import qualified Data.Text as T
import           Path
import           Path.IO
import           Stack.Build.Source
import           Stack.Constants
import           Stack.Exec (defaultEnvSettings)
import           Stack.Package
import           Stack.Types
import           System.Directory (doesFileExist)
import           System.Exit
import           System.IO
import qualified System.Process as P
import           System.Process.Read

-- | Launch a GHCi IDE for the given local project targets with the
-- given options and configure it with the load paths and extensions
-- of those targets.
ide
    :: (HasConfig r, HasBuildConfig r, HasEnvConfig r, MonadReader r m, MonadIO m, MonadThrow m, MonadLogger m, MonadCatch m)
    => [Text] -- ^ Targets.
    -> [String] -- ^ GHC options.
    -> m ()
ide targets useropts = do
    econfig <- asks getEnvConfig
    bconfig <- asks getBuildConfig
    pwd <- getWorkingDir
    pkgs <-
        liftM catMaybes $
        forM (M.toList (bcPackages bconfig)) $
        \(dir,validWanted) ->
             do cabalfp <- getCabalFileName dir
                name <- parsePackageNameFromFilePath cabalfp
                let config =
                        PackageConfig
                        { packageConfigEnableTests = True
                        , packageConfigEnableBenchmarks = True
                        , packageConfigFlags = localFlags mempty bconfig name
                        , packageConfigGhcVersion = envConfigGhcVersion econfig
                        , packageConfigPlatform = configPlatform
                              (getConfig bconfig)
                        }
                pkg <- readPackage config cabalfp
                if validWanted && wanted pwd cabalfp pkg
                    then do
                        pkgOpts <- getPackageOpts (packageOpts pkg) cabalfp
                        srcfiles <-
                            getPackageFiles (packageFiles pkg) Modules cabalfp
                        autogen <- liftM autogenDir (distDirFromDir dir)
                        paths_foo <-
                            liftM
                                (autogen </>)
                                (parseRelFile
                                     ("Paths_" ++
                                      packageNameString name ++ ".hs"))
                        paths_foo_exists <- fileExists paths_foo
                        return
                            (Just
                                 ( packageName pkg
                                 , pkgOpts
                                 , mapMaybe
                                       (stripDir pwd)
                                       (S.toList srcfiles <>
                                        if paths_foo_exists
                                            then [paths_foo]
                                            else [])))
                    else return Nothing
    localdb <- packageDatabaseLocal
    depsdb <- packageDatabaseDeps
    let pkgopts = filter (not . badForGhci) (concat (map _2 pkgs))
        srcfiles = concatMap (map toFilePath . _3) pkgs
        pkgdbs =
            ["--package-db", toFilePath depsdb <> ":" <> toFilePath localdb]
    $logInfo
        ("Configuring GHCi with the following packages: " <>
         T.intercalate ", " (map packageNameText (map _1 pkgs)))
    exec
        "ide-backend-client"
        ("empty" :
         "--local-work-dir" :
         toFilePath pwd :
         map ("--ghc-option=" ++) (pkgopts <> useropts) <> pkgdbs)
        (encode (initialRequest srcfiles))
  where
    wanted pwd cabalfp pkg = isInWantedList || targetsEmptyAndInDir
      where
        isInWantedList = elem (packageNameText (packageName pkg)) targets
        targetsEmptyAndInDir = null targets || isParentOf (parent cabalfp) pwd
    badForGhci x =
        isPrefixOf "-O" x || elem x (words "-debug -threaded -ticky")
    _1 (x,_,_) = x
    _2 (_,x,_) = x
    _3 (_,_,x) = x

-- | Make the initial request.
initialRequest :: [FilePath] -> Value
initialRequest srcfiles =
    object
        [ "request" .= "updateSession"
        , "update" .=
          map
              (\fp ->
                    object
                        [ "update" .= "updateSourceFileFromFile"
                        , "filePath" .= fp])
              srcfiles]

-- | Execute a process within the Stack configured environment.
exec :: (HasConfig r, MonadReader r m, MonadIO m, MonadLogger m, MonadThrow m)
        => String -> [String] -> ByteString -> m b
exec cmd args input = do
    config <- asks getConfig
    menv <-
        liftIO
            (configEnvOverride
                 config
                 defaultEnvSettings
                 { esIncludeGhcPackagePath = False
                 })
    exists <- liftIO $ doesFileExist cmd
    cmd' <-
        if exists
            then return cmd
            else liftM toFilePath $
                 join $ System.Process.Read.findExecutable menv cmd
    let cp =
            (P.proc cmd' args)
            { P.env = envHelper menv
            , P.delegate_ctlc = True
            , P.std_in = P.CreatePipe
            }
    $logProcessRun cmd' args
    (Just procin,Nothing,Nothing,ph) <- liftIO (P.createProcess cp)
    liftIO
        (do hSetBuffering stdin LineBuffering
            hSetBuffering procin LineBuffering)
    liftIO (do S8.hPutStrLn stdout (L.toStrict input)
               S8.hPutStrLn procin (L.toStrict input))
    _tid <-
        liftIO
            (forkIO
                 (forever
                      (do bytes <- S.getLine
                          S.hPutStr procin bytes)))
    ec <- liftIO (P.waitForProcess ph)
    liftIO (exitWith ec)