{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}

-- | A wrapper around hoogle.
module Stack.Hoogle
    ( hoogleCmd
    ) where

import           Stack.Prelude
import qualified Data.ByteString.Char8 as S8
import           Data.Char (isSpace)
import           Data.List (find)
import qualified Data.Set as Set
import qualified Data.Text as T
import           Lens.Micro
import           Path.IO hiding (findExecutable)
import qualified Stack.Build
import           Stack.Fetch
import           Stack.Runners
import           Stack.Types.Config
import           Stack.Types.PackageIdentifier
import           Stack.Types.PackageName
import           Stack.Types.Version
import           System.Exit
import           System.Process.Read (resetExeCache, tryProcessStdout, findExecutable)
import           System.Process.Run

-- | Hoogle command.
hoogleCmd :: ([String],Bool,Bool) -> GlobalOpts -> IO ()
hoogleCmd (args,setup,rebuild) go = withBuildConfig go $ do
    hooglePath <- ensureHoogleInPath
    generateDbIfNeeded hooglePath
    runHoogle hooglePath args
  where
    generateDbIfNeeded :: Path Abs File -> RIO EnvConfig ()
    generateDbIfNeeded hooglePath = do
        databaseExists <- checkDatabaseExists
        if databaseExists && not rebuild
            then return ()
            else if setup || rebuild
                     then do
                         logWarn
                             (if rebuild
                                  then "Rebuilding database ..."
                                  else "No Hoogle database yet. Automatically building haddocks and hoogle database (use --no-setup to disable) ...")
                         buildHaddocks
                         logInfo "Built docs."
                         generateDb hooglePath
                         logInfo "Generated DB."
                     else do
                         logError
                             "No Hoogle database. Not building one due to --no-setup"
                         bail
    generateDb :: Path Abs File -> RIO EnvConfig ()
    generateDb hooglePath = do
        do dir <- hoogleRoot
           createDirIfMissing True dir
           runHoogle hooglePath ["generate", "--local"]
    buildHaddocks :: RIO EnvConfig ()
    buildHaddocks =
        liftIO
            (catch
                 (withBuildConfigAndLock
                      (set
                           (globalOptsBuildOptsMonoidL . buildOptsMonoidHaddockL)
                           (Just True)
                           go)
                      (\lk ->
                            Stack.Build.build
                                (const (return ()))
                                lk
                                defaultBuildOptsCLI))
                 (\(_ :: ExitCode) ->
                       return ()))
    hooglePackageName = $(mkPackageName "hoogle")
    hoogleMinVersion = $(mkVersion "5.0")
    hoogleMinIdent =
        PackageIdentifier hooglePackageName hoogleMinVersion
    installHoogle :: RIO EnvConfig ()
    installHoogle = do
        hooglePackageIdentifier <-
            do (_,_,resolved) <-
                   resolvePackagesAllowMissing

                       -- FIXME this Nothing means "do not follow any
                       -- specific snapshot", which matches old
                       -- behavior. However, since introducing the
                       -- logic to pin a name to a package in a
                       -- snapshot, we may arguably want to ensure
                       -- that we're grabbing the version of Hoogle
                       -- present in the snapshot currently being
                       -- used.
                       Nothing

                       mempty
                       (Set.fromList [hooglePackageName])
               return
                   (case find
                             ((== hooglePackageName) . packageIdentifierName)
                             (map rpIdent resolved) of
                        Just ident@(PackageIdentifier _ ver)
                          | ver >= hoogleMinVersion -> Right ident
                        _ -> Left hoogleMinIdent)
        case hooglePackageIdentifier of
            Left{} ->
                logInfo
                    ("Minimum " <> packageIdentifierText hoogleMinIdent <>
                     " is not in your index. Installing the minimum version.")
            Right ident ->
                logInfo
                    ("Minimum version is " <> packageIdentifierText hoogleMinIdent <>
                     ". Found acceptable " <>
                     packageIdentifierText ident <>
                     " in your index, installing it.")
        config <- view configL
        menv <- liftIO $ configEnvOverride config envSettings
        liftIO
            (catch
                 (withBuildConfigAndLock
                      go
                      (\lk ->
                            Stack.Build.build
                                (const (return ()))
                                lk
                                defaultBuildOptsCLI
                                { boptsCLITargets = [ packageIdentifierText
                                                          (either
                                                               id
                                                               id
                                                               hooglePackageIdentifier)]
                                }))
                 (\(e :: ExitCode) ->
                       case e of
                           ExitSuccess -> resetExeCache menv
                           _ -> throwIO e))
    runHoogle :: Path Abs File -> [String] -> RIO EnvConfig ()
    runHoogle hooglePath hoogleArgs = do
        config <- view configL
        menv <- liftIO $ configEnvOverride config envSettings
        dbpath <- hoogleDatabasePath
        let databaseArg = ["--database=" ++ toFilePath dbpath]
        runCmd
            Cmd
             { cmdDirectoryToRunIn = Nothing
             , cmdCommandToRun = toFilePath hooglePath
             , cmdEnvOverride = menv
             , cmdCommandLineArguments = hoogleArgs ++ databaseArg
             }
            Nothing
    bail :: RIO EnvConfig a
    bail = liftIO (exitWith (ExitFailure (-1)))
    checkDatabaseExists = do
        path <- hoogleDatabasePath
        liftIO (doesFileExist path)
    ensureHoogleInPath :: RIO EnvConfig (Path Abs File)
    ensureHoogleInPath = do
        config <- view configL
        menv <- liftIO $ configEnvOverride config envSettings
        mhooglePath <- findExecutable menv "hoogle"
        eres <- case mhooglePath of
            Nothing -> return $ Left "Hoogle isn't installed."
            Just hooglePath -> do
                result <- tryProcessStdout Nothing menv (toFilePath hooglePath) ["--numeric-version"]
                let unexpectedResult got = Left $ T.concat
                        [ "'"
                        , T.pack (toFilePath hooglePath)
                        , " --numeric-version' did not respond with expected value. Got: "
                        , got
                        ]
                return $ case result of
                    Left err -> unexpectedResult $ T.pack (show err)
                    Right bs -> case parseVersionFromString (takeWhile (not . isSpace) (S8.unpack bs)) of
                        Nothing -> unexpectedResult $ T.pack (S8.unpack bs)
                        Just ver
                            | ver >= hoogleMinVersion -> Right hooglePath
                            | otherwise -> Left $ T.concat
                                [ "Installed Hoogle is too old, "
                                , T.pack (toFilePath hooglePath)
                                , " is version "
                                , versionText ver
                                , " but >= 5.0 is required."
                                ]
        case eres of
            Right hooglePath -> return hooglePath
            Left err
                | setup -> do
                    logWarn $ err <> " Automatically installing (use --no-setup to disable) ..."
                    installHoogle
                    mhooglePath' <- findExecutable menv "hoogle"
                    case mhooglePath' of
                        Just hooglePath -> return hooglePath
                        Nothing -> do
                            logWarn "Couldn't find hoogle in path after installing.  This shouldn't happen, may be a bug."
                            bail
                | otherwise -> do
                    logWarn $ err <> " Not installing it due to --no-setup."
                    bail
    envSettings =
        EnvSettings
        { esIncludeLocals = True
        , esIncludeGhcPackagePath = True
        , esStackExe = True
        , esLocaleUtf8 = False
        }
