module Distribution.Client.Reconfigure (Check (..), reconfigure) where

import Distribution.Client.Compat.Prelude

import Data.Monoid (Any (..))
import System.Directory (doesFileExist)

import Distribution.Simple.Configure (localBuildInfoFile)
import Distribution.Simple.Setup (Flag, flagToMaybe, toFlag)
import Distribution.Simple.Utils
  ( defaultPackageDesc
  , existsAndIsMoreRecentThan
  , info
  )

import Distribution.Client.Config (SavedConfig (..))
import Distribution.Client.Configure (readConfigFlags)
import Distribution.Client.Nix (findNixExpr, inNixShell, nixInstantiate)
import Distribution.Client.Sandbox (findSavedDistPref, updateInstallDirs)
import Distribution.Client.Sandbox.PackageEnvironment
  ( userPackageEnvironmentFile
  )
import Distribution.Client.Setup
  ( ConfigExFlags
  , ConfigFlags (..)
  , GlobalFlags (..)
  )

-- | @Check@ represents a function to check some condition on type @a@. The
-- returned 'Any' is 'True' if any part of the condition failed.
newtype Check a = Check
  { runCheck
      :: Any -- Did any previous check fail?
      -> a -- value returned by previous checks
      -> IO (Any, a) -- Did this check fail? What value is returned?
  }

instance Semigroup (Check a) where
  (<>) c d = Check $ \any0 a0 -> do
    (any1, a1) <- runCheck c any0 a0
    (any2, a2) <- runCheck d (any0 <> any1) a1
    return (any0 <> any1 <> any2, a2)

instance Monoid (Check a) where
  mempty = Check $ \_ a -> return (mempty, a)
  mappend = (<>)

-- | Re-configure the package in the current directory if needed. Deciding
-- when to reconfigure and with which options is convoluted:
--
-- If we are reconfiguring, we must always run @configure@ with the
-- verbosity option we are given; however, that a previous configuration
-- uses a different verbosity setting is not reason enough to reconfigure.
--
-- The package should be configured to use the same \"dist\" prefix as
-- given to the @build@ command, otherwise the build will probably
-- fail. Not only does this determine the \"dist\" prefix setting if we
-- need to reconfigure anyway, but an existing configuration should be
-- invalidated if its \"dist\" prefix differs.
--
-- If the package has never been configured (i.e., there is no
-- LocalBuildInfo), we must configure first, using the default options.
--
-- If the package has been configured, there will be a 'LocalBuildInfo'.
-- If there no package description file, we assume that the
-- 'PackageDescription' is up to date, though the configuration may need
-- to be updated for other reasons (see above). If there is a package
-- description file, and it has been modified since the 'LocalBuildInfo'
-- was generated, then we need to reconfigure.
--
-- The caller of this function may also have specific requirements
-- regarding the flags the last configuration used. For example,
-- 'testAction' requires that the package be configured with test suites
-- enabled. The caller may pass the required settings to this function
-- along with a function to check the validity of the saved 'ConfigFlags';
-- these required settings will be checked first upon determining that
-- a previous configuration exists.
reconfigure
  :: ((ConfigFlags, ConfigExFlags) -> [String] -> GlobalFlags -> IO ())
  -- ^ configure action
  -> Verbosity
  -- ^ Verbosity setting
  -> FilePath
  -- ^ \"dist\" prefix
  -> Flag (Maybe Int)
  -- ^ -j flag for reinstalling add-source deps.
  -> Check (ConfigFlags, ConfigExFlags)
  -- ^ Check that the required flags are set.
  -- If they are not set, provide a message explaining the
  -- reason for reconfiguration.
  -> [String]
  -- ^ Extra arguments
  -> GlobalFlags
  -- ^ Global flags
  -> SavedConfig
  -> IO SavedConfig
reconfigure
  configureAction
  verbosity
  dist
  _numJobsFlag
  check
  extraArgs
  globalFlags
  config =
    do
      savedFlags@(_, _) <- readConfigFlags dist

      useNix <- fmap isJust (findNixExpr globalFlags config)
      alreadyInNixShell <- inNixShell

      if useNix && not alreadyInNixShell
        then do
          -- If we are using Nix, we must reinstantiate the derivation outside
          -- the shell. Eventually, the caller will invoke 'nixShell' which will
          -- rerun cabal inside the shell. That will bring us back to 'reconfigure',
          -- but inside the shell we'll take the second branch, below.

          -- This seems to have a problem: won't 'configureAction' call 'nixShell'
          -- yet again, spawning an infinite tree of subprocesses?
          -- No, because 'nixShell' doesn't spawn a new process if it is already
          -- running in a Nix shell.

          nixInstantiate verbosity dist False globalFlags config
          return config
        else do
          let checks :: Check (ConfigFlags, ConfigExFlags)
              checks =
                checkVerb
                  <> checkDist
                  <> checkOutdated
                  <> check
          (Any frc, flags@(configFlags, _)) <- runCheck checks mempty savedFlags

          let config' :: SavedConfig
              config' = updateInstallDirs (configUserInstall configFlags) config

          when frc $ configureAction flags extraArgs globalFlags
          return config'
    where
      -- Changing the verbosity does not require reconfiguration, but the new
      -- verbosity should be used if reconfiguring.
      checkVerb :: Check (ConfigFlags, b)
      checkVerb = Check $ \_ (configFlags, configExFlags) -> do
        let configFlags' :: ConfigFlags
            configFlags' = configFlags{configVerbosity = toFlag verbosity}
        return (mempty, (configFlags', configExFlags))

      -- Reconfiguration is required if @--build-dir@ changes.
      checkDist :: Check (ConfigFlags, b)
      checkDist = Check $ \_ (configFlags, configExFlags) -> do
        -- Always set the chosen @--build-dir@ before saving the flags,
        -- or bad things could happen.
        savedDist <- findSavedDistPref config (configDistPref configFlags)
        let distChanged :: Bool
            distChanged = dist /= savedDist
        when distChanged $ info verbosity "build directory changed"
        let configFlags' :: ConfigFlags
            configFlags' = configFlags{configDistPref = toFlag dist}
        return (Any distChanged, (configFlags', configExFlags))

      checkOutdated :: Check (ConfigFlags, b)
      checkOutdated = Check $ \_ flags@(configFlags, _) -> do
        let buildConfig :: FilePath
            buildConfig = localBuildInfoFile dist

        -- Has the package ever been configured? If not, reconfiguration is
        -- required.
        configured <- doesFileExist buildConfig
        unless configured $ info verbosity "package has never been configured"

        -- Is the @cabal.config@ file newer than @dist/setup.config@? Then we need
        -- to force reconfigure. Note that it's possible to use @cabal.config@
        -- even without sandboxes.
        userPackageEnvironmentFileModified <-
          existsAndIsMoreRecentThan userPackageEnvironmentFile buildConfig
        when userPackageEnvironmentFileModified $
          info
            verbosity
            ( "user package environment file ('"
                ++ userPackageEnvironmentFile
                ++ "') was modified"
            )

        -- Is the configuration older than the package description?
        descrFile <-
          maybe
            (defaultPackageDesc verbosity)
            return
            (flagToMaybe (configCabalFilePath configFlags))
        outdated <- existsAndIsMoreRecentThan descrFile buildConfig
        when outdated $ info verbosity (descrFile ++ " was changed")

        let failed :: Any
            failed =
              Any outdated
                <> Any userPackageEnvironmentFileModified
                <> Any (not configured)
        return (failed, flags)
