--  Copyright (C) 2004,2007 David Roundy
--
--  This program is free software; you can redistribute it and/or modify
--  it under the terms of the GNU General Public License as published by
--  the Free Software Foundation; either version 2, or (at your option)
--  any later version.
--
--  This program is distributed in the hope that it will be useful,
--  but WITHOUT ANY WARRANTY; without even the implied warranty of
--  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
--  GNU General Public License for more details.
--
--  You should have received a copy of the GNU General Public License
--  along with this program; see the file COPYING.  If not, write to
--  the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
--  Boston, MA 02110-1301, USA.

-- |
-- Copyright   : 2004, 2007 David Roundy
-- License     : GPL
-- Maintainer  : darcs-devel@darcs.net
-- Stability   : experimental
-- Portability : portable

{-# LANGUAGE OverloadedStrings #-}
module Darcs.UI.Commands.Amend
    (
      amend
    , amendrecord
    ) where

import Darcs.Prelude

import Control.Monad ( unless )
import Data.Maybe ( isNothing, isJust )

import Darcs.UI.Commands
    ( DarcsCommand(..), withStdOpts
    , commandAlias
    , nodefaults
    , setEnvDarcsFiles
    , setEnvDarcsPatches
    , amInHashedRepository
    )
import Darcs.UI.Commands.Util
    ( announceFiles
    , historyEditHelp
    , testTentativeAndMaybeExit
    )
import Darcs.UI.Completion ( modifiedFileArgs, knownFileArgs )
import Darcs.UI.Flags ( diffingOpts, pathSetFromArgs )
import Darcs.UI.Options ( Config, (?), (^) )
import qualified Darcs.UI.Options.All as O
import Darcs.UI.PatchHeader
    ( AskAboutDeps(..)
    , HijackOptions(..)
    , patchHeaderConfig
    , runHijackT
    , updatePatchHeader
    )

import Darcs.Repository.Flags ( UpdatePending(..) )
import Darcs.Patch ( RepoPatch, description, PrimOf
                   , effect, invert, invertFL, canonizeFL
                   )
import Darcs.Patch.Apply ( ApplyState )
import Darcs.Patch.Depends ( contextPatches, patchSetUnion, findCommonWithThem )
import Darcs.Patch.Info ( isTag )
import Darcs.Patch.Named ( fmapFL_Named )
import Darcs.Patch.PatchInfoAnd ( hopefully )
import Darcs.Patch.Set ( Origin, PatchSet )
import Darcs.Patch.Split ( primSplitter )
import Darcs.Patch.PatchInfoAnd ( PatchInfoAnd, info, patchDesc )
import Darcs.Patch.Rebase.Fixup ( RebaseFixup(..) )
import Darcs.Patch.Rebase.Name ( RebaseName(..) )
import Darcs.Util.Path ( AnchoredPath )
import Darcs.Repository
    ( Repository
    , AccessType(..)
    , withRepoLock
    , RepoJob(..)
    , identifyRepositoryFor
    , ReadingOrWriting(Reading)
    , tentativelyRemovePatches
    , tentativelyAddPatch
    , withManualRebaseUpdate
    , finalizeRepositoryChanges
    , readPendingAndWorking
    , readPristine
    , readPatches
    , tentativelyRemoveFromPW
    )
import Darcs.Repository.Pending ( readTentativePending, writeTentativePending )
import Darcs.Repository.Prefs ( getDefaultRepo )
import Darcs.UI.SelectChanges
    ( WhichChanges(..)
    , selectionConfigPrim
    , runInvertibleSelection
    , withSelectedPatchFromList
    )
import qualified Darcs.UI.SelectChanges as S
    ( PatchSelectionOptions(..)
    )
import Darcs.Util.Exception ( clarifyErrors )
import Darcs.Patch.Witnesses.Ordered
    ( FL(..), RL, (:>)(..), (+>+)
    , nullFL, reverseRL, reverseFL, mapFL_FL
    )
import Darcs.Patch.Witnesses.Sealed ( Sealed(..) )

import Darcs.Util.English ( anyOfClause, itemizeVertical )
import Darcs.Util.Printer ( Doc, formatWords, putDocLn, text, (<+>), ($$), ($+$) )
import Darcs.Util.Printer.Color ( ePutDocLn )
import Darcs.Util.Tree( Tree )


amendDescription :: String
amendDescription = "Improve a patch before it leaves your repository."


amendHelp :: Doc
amendHelp =
  formatWords
  [ "Amend updates a \"draft\" patch with additions or improvements,"
  , "resulting in a single \"finished\" patch."
  ]
  $+$ formatWords
  [ "By default `amend` proposes you to record additional changes."
  , "If instead you want to remove changes, use the flag `--unrecord`."
  ]
  $+$ formatWords
  [ "When recording a draft patch, it is a good idea to start the name with"
  , "`DRAFT:`. When done, remove it with `darcs amend --edit-long-comment`."
  , "Alternatively, to change the patch name without starting an editor, "
  , "use the `--name`/`-m` flag:"
  ]
  $+$ text
    "    darcs amend --match 'name \"DRAFT: foo\"' --name 'foo2'"
  $+$ formatWords
  [ "Like `darcs record`, if you call amend with files as arguments,"
  , "you will only be asked about changes to those files.  So to amend a"
  , "patch to foo.c with improvements in bar.c, you would run:"
  ]
  $+$ text
    "    darcs amend --match 'touch foo.c' bar.c"
  $+$ historyEditHelp

amend :: DarcsCommand
amend = DarcsCommand
    {
      commandProgramName          = "darcs"
    , commandName                 = "amend"
    , commandHelp                 = amendHelp
    , commandDescription          = amendDescription
    , commandExtraArgs            = -1
    , commandExtraArgHelp         = ["[FILE or DIRECTORY]..."]
    , commandCommand              = amendCmd
    , commandPrereq               = amInHashedRepository
    , commandCompleteArgs         = fileArgs
    , commandArgdefaults          = nodefaults
    , commandOptions              = allOpts
    }
  where
    fileArgs fps flags args =
      if (O.amendUnrecord ? flags)
        then knownFileArgs fps flags args
        else modifiedFileArgs fps flags args
    basicOpts
      = O.amendUnrecord
      ^ O.notInRemote
      ^ O.matchOneNontag
      ^ O.testChanges
      ^ O.interactive --True
      ^ O.author
      ^ O.selectAuthor
      ^ O.patchname
      ^ O.askDeps
      ^ O.askLongComment
      ^ O.keepDate
      ^ O.lookforadds
      ^ O.lookforreplaces
      ^ O.lookformoves
      ^ O.repoDir
      ^ O.diffAlgorithm
    advancedOpts
      = O.umask
      ^ O.setScriptsExecutable
    allOpts = withStdOpts basicOpts advancedOpts
    amendCmd fps flags args = pathSetFromArgs fps args >>= doAmend flags

amendrecord :: DarcsCommand
amendrecord = commandAlias "amend-record" Nothing amend

doAmend :: Config -> Maybe [AnchoredPath] -> IO ()
doAmend cfg files =
  withRepoLock (O.useCache ? cfg) (O.umask ? cfg) $
      RepoJob $ \(repository :: Repository 'RW p wU wR) -> do
    patchSet <- readPatches repository
    _ :> candidates <- filterNotInRemote cfg repository patchSet
    withSelectedPatchFromList "amend" candidates (patchSelOpts cfg) $
     \(kept :> oldp) -> do
      announceFiles (O.verbosity ? cfg) files "Amending changes in"
      pending :> working <-
        readPendingAndWorking (diffingOpts cfg) repository files
      -- auxiliary function needed because the witness types differ for the
      -- isTag case
      let go :: FL (PrimOf p) wR wU1 -> IO ()
          go NilFL | not (hasEditMetadata cfg) = putInfo cfg "No changes!"
          go ch = do
            let selection_config =
                   selectionConfigPrim First "record"
                       (patchSelOpts cfg)
                       (Just (primSplitter (O.diffAlgorithm ? cfg)))
                       files
            (chosenPatches :> _) <- runInvertibleSelection ch selection_config
            addChangesToPatch cfg repository kept oldp chosenPatches pending working
      if not (isTag (info oldp))
        -- amending a normal patch
        then
          if O.amendUnrecord ? cfg
            then do
              let selection_config =
                    selectionConfigPrim Last "unrecord" (patchSelOpts cfg)
                      (Just (primSplitter (O.diffAlgorithm ? cfg)))
                      files
              (_ :> chosenPrims) <-
                runInvertibleSelection (effect oldp) selection_config
              let invPrims = reverseRL (invertFL chosenPrims)
              addChangesToPatch cfg repository kept oldp invPrims pending working
            else
              go (canonizeFL (O.diffAlgorithm ? cfg) (pending +>+ working))
        -- amending a tag
        else
          if hasEditMetadata cfg && isNothing files
            -- the user is not trying to add new changes to the tag so there is
            -- no reason to warn.
            then go NilFL
            -- the user is trying to add new changes to a tag.
            else do
              if hasEditMetadata cfg
                -- the user already knows that it is possible to edit tag metadata,
                -- note that s/he is providing editing options!
                then ePutDocLn "You cannot add new changes to a tag."
                -- the user may not be aware that s/he can edit tag metadata.
                else
                  ePutDocLn
                    "You cannot add new changes to a tag, but you are allowed to edit tag's metadata (see darcs help amend)."
              go NilFL


addChangesToPatch
  :: (RepoPatch p, ApplyState p ~ Tree)
  => Config
  -> Repository 'RW p wU wR
  -> RL (PatchInfoAnd p) wC wX  -- ^ candidates for --ask-deps
  -> PatchInfoAnd p wX wR       -- ^ original patch
  -> FL (PrimOf p) wR wY        -- ^ changes to add
  -> FL (PrimOf p) wR wP        -- ^ pending
  -> FL (PrimOf p) wP wU        -- ^ working
  -> IO ()
addChangesToPatch cfg _repository context oldp chs pending working =
  if nullFL chs && not (hasEditMetadata cfg)
    then putInfo cfg "You don't want to record anything!"
    else do
      -- remember the old pending for the amend --unrecord case, see below
      Sealed old_pending <- readTentativePending _repository
      -- If a rebase is in progress, we want to manually update the rebase
      -- state, using the amendments directly as rebase fixups. This is
      -- necessary because otherwise we will first remove the original patch
      -- then add the amended patch,
      -- and this can lead to more conflicts than using the amendment as a fixup
      -- directly. For example, if a rename operation is amended in, the rename
      -- can be propagated to any edits to the file in the rebase state, whereas
      -- a delete then add would just cause a conflict.
      -- 
      -- We can also signal that any explicit dependencies of the old patch
      -- should be rewritten for the new patch using a 'NameFixup'.
      (_repository, (mlogf, newp)) <-
        withManualRebaseUpdate _repository $ \_repository -> do
          -- Note we pass NoUpdatePending here and below when re-adding the
          -- amended patch, and instead fix pending explicitly further below.
          _repository <-
            tentativelyRemovePatches
              _repository
              NoUpdatePending
              (oldp :>: NilFL)
          (mlogf, newp) <-
            runHijackT AlwaysRequestHijackPermission $
            updatePatchHeader
              "amend"
              (if O.askDeps ? cfg
                 then AskAboutDeps context
                 else NoAskAboutDeps)
              (patchSelOpts cfg)
              (patchHeaderConfig cfg)
              (fmapFL_Named effect (hopefully oldp))
              chs
          let fixups =
                mapFL_FL PrimFixup (invert chs) +>+
                NameFixup (Rename (info newp) (info oldp)) :>:
                NilFL
          setEnvDarcsFiles newp
          _repository <-
            tentativelyAddPatch _repository NoUpdatePending newp
          return (_repository, fixups, (mlogf, newp))
      let failmsg = maybe "" (\lf -> "\nLogfile left in " ++ lf ++ ".") mlogf
      tp <- readPristine _repository
      testTentativeAndMaybeExit tp cfg
        ("you have a bad patch: '" ++ patchDesc newp ++ "'")
        "amend it"
        (Just failmsg)
      if O.amendUnrecord ? cfg then
        writeTentativePending _repository $ invert chs +>+ old_pending
      else
        tentativelyRemoveFromPW _repository chs pending working
      _repository <-
        finalizeRepositoryChanges _repository (O.dryRun ? cfg)
          `clarifyErrors` failmsg
      case O.verbosity ? cfg of
        O.NormalVerbosity -> putDocLn "Finished amending patch."
        O.Verbose -> putDocLn $ "Finished amending patch:" $$ description newp
        _ -> return ()
      setEnvDarcsPatches (newp :>: NilFL)

filterNotInRemote :: RepoPatch p
                  => Config
                  -> Repository 'RW p wU wR
                  -> PatchSet p Origin wR
                  -> IO ((PatchSet p :> RL (PatchInfoAnd p)) Origin wR)
filterNotInRemote cfg repository patchSet = do
    nirs <- mapM getNotInRemotePath (O.notInRemote ? cfg)
    if null nirs
      then
        -- We call contextPatches here because
        -- (a) selecting patches beyond the latest clean tag is impossible anyway
        -- (b) makes it easier to reconstruct a PatchSet w/o the selected patch
        -- (c) avoids listing the complete list of patches in the repo when user
        --     rejects the last selectable patch
        return (contextPatches patchSet)
      else do
        putInfo cfg $
          "Determining patches not in" <+> anyOfClause nirs $$ itemizeVertical 2 nirs
        Sealed thems <- patchSetUnion `fmap` mapM readNir nirs
        in_remote :> only_ours <- return $ findCommonWithThem patchSet thems
        return (in_remote :> reverseFL only_ours)
  where
    readNir loc = do
      repo <- identifyRepositoryFor Reading repository (O.useCache ? cfg) loc
      rps <- readPatches repo
      return (Sealed rps)
    getNotInRemotePath (O.NotInRemotePath p) = return p
    getNotInRemotePath O.NotInDefaultRepo = do
        defaultRepo <- getDefaultRepo
        let err = fail $ "No default push/pull repo configured, please pass a "
                         ++ "repo name to --" ++ O.notInRemoteFlagName
        maybe err return defaultRepo

hasEditMetadata :: Config -> Bool
hasEditMetadata cfg = isJust (O.author ? cfg)
                    || O.selectAuthor ? cfg
                    || isJust (O.patchname ? cfg)
                    || O.askLongComment ? cfg == Just O.YesEditLongComment
                    || O.askLongComment ? cfg == Just O.PromptLongComment
                    || O.askDeps ? cfg

patchSelOpts :: Config -> S.PatchSelectionOptions
patchSelOpts cfg = S.PatchSelectionOptions
    { S.verbosity = O.verbosity ? cfg
    , S.matchFlags = O.matchOneNontag ? cfg
    , S.interactive = isInteractive cfg
    , S.selectDeps = O.PromptDeps -- option not supported, use default
    , S.withSummary = O.NoSummary -- option not supported, use default
    }

isInteractive :: Config -> Bool
isInteractive cfg = maybe True id (O.interactive ? cfg)

putInfo :: Config -> Doc -> IO ()
putInfo cfg what = unless (O.verbosity ? cfg == O.Quiet) $ putDocLn what
