--  Copyright (C) 2003-2004 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.

module Darcs.UI.Commands.Diff ( diffCommand ) where

import Darcs.Prelude hiding ( all )

import Control.Monad ( unless, when )
import Data.Maybe ( fromMaybe )
import Data.Maybe ( isJust )
import System.Directory ( createDirectory, findExecutable, withCurrentDirectory )
import System.FilePath.Posix ( takeFileName, (</>) )
import System.IO ( hFlush, stdout )

import Darcs.Patch ( listTouchedFiles )
import Darcs.Patch.Apply ( Apply(..) )
import Darcs.Patch.Depends ( findCommonWithThem )
import Darcs.Patch.Info ( displayPatchInfo )
import Darcs.Patch.Match ( matchFirstPatchset, matchSecondPatchset, secondMatch )
import Darcs.Patch.Named ( anonymous )
import Darcs.Patch.PatchInfoAnd ( info, n2pia )
import Darcs.Patch.Set ( patchSetSnoc )
import Darcs.Patch.Witnesses.Ordered ( (:>)(..), mapFL )
import Darcs.Patch.Witnesses.Sealed ( Sealed(..), seal )
import Darcs.Repository ( RepoJob(..), readPatches, withRepository )
import Darcs.Repository.State
    ( applyTreeFilter
    , readPristine
    , restrictSubpaths
    , unrecordedChanges
    )
import Darcs.UI.Commands
    ( DarcsCommand(..)
    , amInHashedRepository
    , nodefaults
    , withStdOpts
    )
import Darcs.UI.Completion ( knownFileArgs )
import Darcs.UI.External ( diffProgram )
import Darcs.UI.Flags ( DarcsFlag, diffingOpts, pathSetFromArgs, useCache, wantGuiPause )
import Darcs.UI.Options ( parseFlags, (?), (^) )
import qualified Darcs.UI.Options.All as O
import Darcs.Util.Cache ( mkDirCache )
import Darcs.Util.CommandLine ( parseCmd )
import Darcs.Util.Exec ( execInteractive )
import Darcs.Util.Global ( debugMessage )
import Darcs.Util.Lock ( withTempDir )
import Darcs.Util.Path ( AbsolutePath, AnchoredPath, isPrefix, toFilePath )
import Darcs.Util.Printer ( Doc, putDocLn, text, vcat )
import Darcs.Util.Prompt ( askEnter )
import Darcs.Util.Tree.Hashed ( hashedTreeIO, writeDarcsHashed )
import Darcs.Util.Tree.Plain ( writePlainTree )
import Darcs.Util.Workaround ( getCurrentDirectory )

diffDescription :: String
diffDescription = "Create a diff between two versions of the repository."

diffHelp :: Doc
diffHelp = text $
 "The `darcs diff` command compares two versions of the working tree of\n" ++
 "the current repository.  Without options, the pristine (recorded) and\n" ++
 "unrecorded working trees are compared.  This is lower-level than\n" ++
 "the `darcs whatsnew` command, since it outputs a line-by-line diff,\n" ++
 "and it is also slower.  As with `darcs whatsnew`, if you specify\n" ++
 "files or directories, changes to other files are not listed.\n" ++
 "The command always uses an external diff utility.\n" ++
 "\n" ++
 "With the `--patch` option, the comparison will be made between working\n" ++
 "trees with and without that patch.  Patches *after* the selected patch\n" ++
 "are not present in either of the compared working trees.  The\n" ++
 "`--from-patch` and `--to-patch` options allow the set of patches in the\n" ++
 "`old' and `new' working trees to be specified separately.\n" ++
 "\n" ++
 "The associated tag and match options are also understood, e.g. `darcs\n" ++
 "diff --from-tag 1.0 --to-tag 1.1`.  All these options assume an\n" ++
 "ordering of the patch set, so results may be affected by operations\n" ++
 "such as `darcs optimize reorder`.\n" ++
 "\n" ++
 "diff(1) is always called with the arguments `-rN` and by default also\n" ++
 "with `-u` to show the differences in unified format. This can be turned\n" ++
 "off by passing `--no-unified`. An additional argument can be passed\n" ++
 "using `--diff-opts`, such as `--diff-opts=-ud` or `--diff-opts=-wU9`.\n" ++
 "\n" ++
 "The `--diff-command` option can be used to specify an alternative\n" ++
 "utility. Arguments may be included, separated by whitespace.  The value\n" ++
 "is not interpreted by a shell, so shell constructs cannot be used.  The\n" ++
 "arguments %1 and %2 MUST be included, these are substituted for the two\n" ++
 "working trees being compared. For instance:\n" ++
 "\n" ++
 "    darcs diff -p . --diff-command \"meld %1 %2\"\n" ++
 "\n" ++
 "If this option is used, `--diff-opts` is ignored.\n"

diffCommand :: DarcsCommand
diffCommand = DarcsCommand
    { commandProgramName = "darcs"
    , commandName = "diff"
    , commandHelp = diffHelp
    , commandDescription = diffDescription
    , commandExtraArgs = -1
    , commandExtraArgHelp = ["[FILE or DIRECTORY]..."]
    , commandCommand = diffCmd
    , commandPrereq = amInHashedRepository
    , commandCompleteArgs = knownFileArgs
    , commandArgdefaults = nodefaults
    , commandOptions = withStdOpts diffBasicOpts diffAdvancedOpts
    }
  where
    diffBasicOpts
      = O.matchOneOrRange
      ^ O.extDiff
      ^ O.lookforadds
      ^ O.lookformoves
      ^ O.repoDir
      ^ O.storeInMemory
    diffAdvancedOpts = O.pauseForGui

diffCmd :: (AbsolutePath, AbsolutePath) -> [DarcsFlag] -> [String] -> IO ()
diffCmd fps opts args
  | not (null (O.matchLast ? opts)) &&
      not (null (O.matchFrom ? opts)) =
        fail $ "using --patch and --last at the same time with the 'diff'" ++
          " command doesn't make sense. Use --from-patch to create a diff" ++
          " from this patch to the present, or use just '--patch' to view" ++
          " this specific patch."
  | otherwise = doDiff opts =<< pathSetFromArgs fps args

doDiff :: [DarcsFlag] -> Maybe [AnchoredPath] ->  IO ()
doDiff opts mpaths = withRepository (useCache ? opts) $ RepoJob $ \repository -> do
  patchset <- readPatches repository
  debugMessage "After readPatches"
  -- We pass @mpaths@ here which means we get only the changes that affect the
  -- given paths (if any).
  unrecorded <- unrecordedChanges (diffingOpts opts) repository mpaths
  debugMessage "After getting the unrecorded changes"
  -- Use of 'anonymous' is unproblematic here as we don't store any patches.
  -- But we must take care not to show its fake patch info to the user.
  unrecorded' <- n2pia `fmap` anonymous unrecorded

  let matchFlags = parseFlags O.matchOneOrRange opts

  -- If no secondMatch (--to-xxx) is specified, include unrecorded changes
  Sealed all <- return $
    if secondMatch matchFlags
      then seal patchset
      else seal $ patchSetSnoc patchset unrecorded'
  -- Note how this differs from how firstMatch defaults for log command
  Sealed ctx <- return $
    fromMaybe (seal patchset) $ matchFirstPatchset matchFlags patchset
  Sealed match <- return $
    fromMaybe (seal all) $ matchSecondPatchset matchFlags patchset

  _ :> todiff <- return $ findCommonWithThem match ctx
  _ :> tounapply <- return $ findCommonWithThem all match

  Sealed logmatch <- return $
    if secondMatch matchFlags
      then seal match
      else seal patchset
  -- Same as @todiff@ but without trailing @unrecorded'@ changes
  _ :> tolog <- return $ findCommonWithThem logmatch ctx

  let touched = listTouchedFiles todiff
      files = case mpaths of
               Nothing -> touched
               Just paths ->
                  concatMap (\path -> filter (isPrefix path) touched) paths
  relevant <- restrictSubpaths repository files

  formerdir <- getCurrentDirectory
  let thename = takeFileName formerdir
  withTempDir "darcs-diff" $ \tmpdir -> do
      getCurrentDirectory >>= debugMessage . ("doDiff: I am now in "++)
      let tdir = toFilePath tmpdir
      let odir = tdir </> ("old-"++thename)
      createDirectory odir
      let ndir = tdir </> ("new-"++thename)
      createDirectory ndir

      -- Prepare the (plain) trees we want to compare. Since we need to access
      -- our repository, we have to restore the working directory.
      withCurrentDirectory formerdir $ do
        -- Make sure we have at least one writable cache entry
        -- to serve as our storage for hashed 'Tree' items
        -- during the 'apply' and 'unapply' operations below.
        let cache = mkDirCache tdir
        pristine <- readPristine repository
        -- fill our temporary cache
        _ <- writeDarcsHashed pristine cache

        -- @base@ will be like our working tree, /except/ that it contains only
        -- the unrecorded changes that affect the given file paths, see comment
        -- above when we called 'unrecordedChanges'.
        base <- if secondMatch matchFlags
                 then return pristine
                 else snd <$> hashedTreeIO (apply unrecorded') pristine cache

        newtree <- snd <$> hashedTreeIO (unapply tounapply) base cache
        -- @todiff@ may have our @unrecorded'@ changes as its last element. If
        -- we used our full working tree as @base@, then we would now unapply
        -- filtered changes from an unfiltered 'Tree', so the result would be
        -- the pristine Tree with the filtered-out unrecorded changes /still
        -- applied/. Unapplying the (unfiltered) recorded changes that touch
        -- paths that we filtered out would then fail (issue2639).
        -- We cannot use 'readUnrecorded' and pass it @mpaths@ because that
        -- would filter the whole Tree, so again unapplying recorded changes
        -- that touch irrelevant paths would fail.
        -- A valid alternative solution would be to not pre-filter unrecorded
        -- changes at all, since we filter the resulting Trees anyway (see
        -- below). But that may be less efficient if there are many unrecorded
        -- changes but we are interested in just a small subset of the affected
        -- paths.
        oldtree <- snd <$> hashedTreeIO (unapply todiff) newtree cache

        writePlainTree (applyTreeFilter relevant oldtree) (toFilePath odir)
        writePlainTree (applyTreeFilter relevant newtree) (toFilePath ndir)
        -- Display patch info for (only) the recorded patches that we diff
        putDocLn $ vcat $ map displayPatchInfo $ reverse $ mapFL info tolog
        hFlush stdout

      -- Call the external diff program. Note we are now back in our
      -- temporary directory.
      cmd <- diffProgram
      let old = takeFileName $ toFilePath odir
          new = takeFileName $ toFilePath ndir
      case getDiffCmdAndArgs cmd opts old new of
        Left err -> fail err
        Right (d_cmd, d_args) -> do
          cmdExists <- findExecutable d_cmd
          unless (isJust cmdExists) $
            fail $ d_cmd ++ " is not an executable in --diff-command"
          let pausingForGui = (wantGuiPause opts == O.YesWantGuiPause)
              cmdline = unwords (d_cmd : d_args)
          when pausingForGui $ putStrLn $ "Running command '" ++ cmdline ++ "'"
          _ <- execInteractive cmdline Nothing
          when pausingForGui $ askEnter "Hit return to move on..."

-- | Returns the command we should use for diff as a tuple (command, arguments).
-- This will either be whatever the user specified via --diff-command  or the
-- default 'diffProgram'.  Note that this potentially involves parsing the
-- user's diff-command, hence the possibility for failure.
getDiffCmdAndArgs :: String -> [DarcsFlag] -> String -> String
                  -> Either String (String, [String])
getDiffCmdAndArgs cmd opts f1 f2 = helper (O.extDiff ? opts) where
  helper extDiff =
    case O.diffCmd extDiff of
      Just c ->
        case parseCmd [ ('1', f1) , ('2', f2) ] c of
          Left err      -> Left $ show err
          Right ([],_)  -> error "parseCmd should never return empty list"
          Right (cmd':args,_)
            | length (filter (== f1) args) == 1
            , length (filter (== f2) args) == 1 -> Right (cmd',args)
            | otherwise -> Left $ "Invalid argument (%1 or %2) in --diff-command"
      Nothing -> -- if no command specified, use 'diff'
        Right (cmd, "-rN":getDiffOpts extDiff++[f1,f2])

getDiffOpts :: O.ExternalDiff -> [String]
getDiffOpts O.ExternalDiff {O.diffOptions=os,O.diffUnified=u} = addUnified os where
  addUnified = if u then ("-u":) else id
