--  Copyright (C) 2009-2012 Ganesh Sittampalam
--
--  BSD3
{-# LANGUAGE OverloadedStrings #-}
module Darcs.Repository.Rebase
    ( -- * Create/read/write rebase patch
      readTentativeRebase
    , writeTentativeRebase
    , withTentativeRebase
    , readRebase
    , finalizeTentativeRebase
    , revertTentativeRebase
    , withManualRebaseUpdate
      -- * Handle rebase format and status
    , checkHasRebase
    , displayRebaseStatus
    , updateRebaseFormat
      -- * Handle old-style rebase
    , extractOldStyleRebase
    , checkOldStyleRebaseStatus
    ) where

import Darcs.Prelude

import Control.Monad ( unless, void, when )
import System.Directory ( copyFile, renameFile )
import System.Exit ( exitFailure )
import System.FilePath.Posix ( (</>) )

import qualified Darcs.Patch.Rebase.Legacy.Wrapped as W
import Darcs.Patch.PatchInfoAnd
    ( PatchInfoAnd
    , PatchInfoAndG
    , fmapPIAP
    , hopefully
    )
import Darcs.Patch.Rebase.Suspended
    ( Suspended(Items)
    , countToEdit
    , readSuspended
    , showSuspended
    , simplifyPushes
    , removeFixupsFromSuspended
    )
import Darcs.Patch.Rebase.Fixup ( RebaseFixup(..) )
import Darcs.Patch.RepoPatch ( RepoPatch, PrimOf )
import Darcs.Patch.Show ( ShowPatchFor(ForStorage) )
import Darcs.Patch.Witnesses.Ordered
    ( (:>)(..)
    , FL(..)
    , RL(..)
    , foldlwFL
    , mapRL_RL
    , (+<<+)
    )
import Darcs.Patch.Witnesses.Sealed ( Dup(..) )

import Darcs.Repository.Format
    ( RepoProperty ( RebaseInProgress_2_16, RebaseInProgress )
    , formatHas
    , addToFormat
    , removeFromFormat
    )
import Darcs.Repository.InternalTypes
    ( Repository
    , AccessType(..)
    , modifyRepoFormat
    , repoFormat
    , repoLocation
    )
import Darcs.Repository.Paths
    ( rebasePath
    , tentativeRebasePath
    )

import Darcs.Util.Diff ( DiffAlgorithm(MyersDiff) )
import Darcs.Util.English ( englishNum, Noun(..) )
import Darcs.Util.Exception ( catchDoesNotExistError )
import Darcs.Util.Lock ( writeDocBinFile, readBinFile )
import Darcs.Util.Parser ( parse )
import Darcs.Util.Printer ( text, hsep, vcat )
import Darcs.Util.Printer.Color ( ePutDocLn )
import Darcs.Util.URL ( isValidLocalPath )

withManualRebaseUpdate
   :: RepoPatch p
   => Repository rt p wU wR
   -> (Repository rt p wU wR -> IO (Repository rt p wU wR', FL (RebaseFixup (PrimOf p)) wR' wR, x))
   -> IO (Repository rt p wU wR', x)
withManualRebaseUpdate r subFunc = do
    susp <- readTentativeRebase r
    (r', fixups, x) <- subFunc r
    when (countToEdit susp > 0) $
      -- HACK overwrite the changes that were made by subFunc
      -- which may and indeed does call add/remove patch
      writeTentativeRebase r' (simplifyPushes MyersDiff fixups susp)
    return (r', x)

-- | Fail if there is an old-style rebase present.
-- To be called initially for every command except rebase upgrade.
checkOldStyleRebaseStatus :: Repository rt p wU wR -> IO ()
checkOldStyleRebaseStatus repo = do
  let rf = repoFormat repo
  when (formatHas RebaseInProgress rf) $ do
      ePutDocLn upgradeMsg
      exitFailure
  where
    upgradeMsg = vcat
      [ "An old-style rebase is in progress in this repository. You can upgrade it"
      , "to the new format using the 'darcs rebase upgrade' command. The repository"
      , "format is unaffected by this, but you won't be able to use a darcs version"
      , "older than 2.16 on this repository until the current rebase is finished."
      ]

-- | Fail unless we already have some suspended patches.
-- Not essential, since all rebase commands should be happy to work
-- with an empty rebase state.
checkHasRebase :: Repository rt p wU wR -> IO ()
checkHasRebase repo =
  unless (formatHas RebaseInProgress_2_16 $ repoFormat repo) $
    fail "No rebase in progress. Try 'darcs rebase suspend' first."

-- | Report the rebase status if there is (still) a rebase in progress
-- after the command has finished running.
-- To be called via 'finally' for every 'RepoJob'.
displayRebaseStatus :: RepoPatch p => Repository rt p wU wR -> IO ()
displayRebaseStatus repo = do
  -- The repoLocation may be a remote URL (e.g. darcs log). We neither can nor
  -- want to display anything in that case.
  when (isValidLocalPath $ repoLocation repo) $ do
    -- Why do we use 'readRebase' and not 'readTentativeRebase' here?
    -- There are three cases:
    -- * We had no transaction in the first place.
    -- * We had a successful transaction: then it will be finalized before we
    --   are called (because finalization is part of the RepoJob itself) and
    --   we want to report the new finalized state.
    -- * We had a transaction that was cancelled or failed: then we want to
    --   report the old (unmodified) rebase state.
    -- Thus, in all cases 'readRebase' is the correct choice. However, if there
    -- is no rebase in progress, then 'rebasePath' may not exist, so we must
    -- handle that.
    suspended <- readRebase repo `catchDoesNotExistError` return (Items NilFL)
    case countToEdit suspended of
      0 -> return ()
      count ->
        ePutDocLn $ hsep
          [ "Rebase in progress:"
          , text (show count)
          , "suspended"
          , text (englishNum count (Noun "patch") "")
          ]

-- | Rebase format update for all commands that modify the repo,
-- except rebase upgrade. This is called by 'finalizeRepositoryChanges'.
updateRebaseFormat :: RepoPatch p => Repository 'RW p wU wR -> IO ()
updateRebaseFormat repo = do
  let rf = repoFormat repo
      hadRebase = formatHas RebaseInProgress_2_16 rf
  suspended <-
    readTentativeRebase repo `catchDoesNotExistError` return (Items NilFL)
  case countToEdit suspended of
    0 ->
      when hadRebase $ do
        void $ modifyRepoFormat (removeFromFormat RebaseInProgress_2_16) repo
        putStrLn "Rebase finished!"
    _ ->
      unless hadRebase $
        void $ modifyRepoFormat (addToFormat RebaseInProgress_2_16) repo

withTentativeRebase
  :: RepoPatch p
  => Repository rt p wU wR
  -> Repository rt p wU wR'
  -> (Suspended p wR -> Suspended p wR')
  -> IO ()
withTentativeRebase r r' f =
  readTentativeRebase r >>= writeTentativeRebase r' . f

readTentativeRebase :: RepoPatch p
                    => Repository rt p wU wR -> IO (Suspended p wR)
readTentativeRebase = readRebaseFile tentativeRebasePath

writeTentativeRebase :: RepoPatch p
                     => Repository rt p wU wR -> Suspended p wR -> IO ()
writeTentativeRebase = writeRebaseFile tentativeRebasePath

readRebase :: RepoPatch p => Repository rt p wU wR -> IO (Suspended p wR)
readRebase = readRebaseFile rebasePath

createTentativeRebase :: RepoPatch p => Repository rt p wU wR -> IO ()
createTentativeRebase r = writeRebaseFile tentativeRebasePath r (Items NilFL)

revertTentativeRebase :: RepoPatch p => Repository rt p wU wR -> IO ()
revertTentativeRebase repo =
  copyFile rebasePath tentativeRebasePath
    `catchDoesNotExistError` createTentativeRebase repo

finalizeTentativeRebase :: IO ()
finalizeTentativeRebase = renameFile tentativeRebasePath rebasePath

-- unsafe witnesses, not exported
readRebaseFile :: RepoPatch p
               => FilePath -> Repository rt p wU wR -> IO (Suspended p wX)
readRebaseFile path r = do
  parsed <- parse readSuspended <$> readBinFile (repoLocation r </> path)
  case parsed of
    Left e -> fail $ unlines ["parse error in file " ++ path, e]
    Right (result, _) -> return result

-- unsafe witnesses, not exported
writeRebaseFile :: RepoPatch p
                => FilePath -> Repository rt p wU wR
                -> Suspended p wR -> IO ()
writeRebaseFile path r sp =
  writeDocBinFile (repoLocation r </> path) (showSuspended ForStorage sp)

type PiaW p = PatchInfoAndG (W.WrappedNamed p)

extractOldStyleRebase :: forall p wA wB. RepoPatch p
                      => RL (PiaW p) wA wB
                      -> Maybe ((RL (PatchInfoAnd p) :> Dup (Suspended p)) wA wB)
extractOldStyleRebase ps = go (ps :> NilFL) where
  go :: (RL (PiaW p) :> FL (PatchInfoAnd p)) wA wB
     -> Maybe ((RL (PatchInfoAnd p) :> Dup (Suspended p)) wA wB)
  go (NilRL :> _) = Nothing
  go (xs :<: x :> ys)
    | W.RebaseP _ r <- hopefully x = do
      let xs' = mapRL_RL (fmapPIAP W.fromRebasing) xs
          rffs = foldlwFL (removeFixupsFromSuspended . hopefully) ys
      return ((xs' +<<+ ys) :> Dup (rffs r))
    | otherwise = go (xs :> fmapPIAP W.fromRebasing x :>: ys)
