--  Copyright (C) 2009 Ganesh Sittampalam
--
--  BSD3
module Darcs.Patch.Rebase.Change
    ( RebaseChange(..)
    , toRebaseChanges
    , extractRebaseChange
    , reifyRebaseChange
    , partitionUnconflicted
    , rcToPia
    , WithDroppedDeps(..)
    , WDDNamed
    , commuterIdWDD
    , simplifyPush, simplifyPushes
    , addNamedToRebase
    ) where

import Darcs.Prelude

import Darcs.Patch.Commute ( commuteFL, commuteRL )
import Darcs.Patch.CommuteFn
    ( CommuteFn
    , MergeFn
    , commuterFLId, commuterIdFL
    )
import Darcs.Patch.Debug ( PatchDebug(..) )
import Darcs.Patch.Effect ( Effect(..) )
import Darcs.Patch.FileHunk ( IsHunk(..) )
import Darcs.Patch.Format ( PatchListFormat(..) )
import Darcs.Patch.Ident ( Ident(..), PatchId )
import Darcs.Patch.Info ( PatchInfo, patchinfo, displayPatchInfo )
import Darcs.Patch.Invert ( Invert, invert, invertFL )
import Darcs.Patch.Merge ( selfMerger )
import Darcs.Patch.Named
    ( Named(..)
    , HasDeps(..)
    , infopatch
    , mergerIdNamed
    , patchcontents
    , ShowDepsFormat(..)
    , showDependencies
    )

import Darcs.Patch.PatchInfoAnd ( PatchInfoAnd, PatchInfoAndG, n2pia )
import Darcs.Patch.Apply ( Apply(..) )
import Darcs.Patch.Commute ( Commute(..) )
import Darcs.Patch.Inspect ( PatchInspect(..) )
import Darcs.Patch.Read ( ReadPatch(..) )
import Darcs.Patch.Show ( ShowPatch(..), displayPatch )
import Darcs.Patch.Summary
    ( ConflictState(..)
    , IsConflictedPrim(..)
    , Summary(..)
    , plainSummary
    , plainSummaryFL
    )
import Darcs.Patch.FromPrim ( PrimPatchBase(..), FromPrim(..) )
import Darcs.Patch.Permutations ( genCommuteWhatWeCanFL )
import Darcs.Patch.Prim.Class ( PrimPatch )
import Darcs.Patch.Rebase.Fixup
    ( RebaseFixup(..)
    , commuteFixupNamed, commuteNamedFixup
    , flToNamesPrims
    , pushFixupFixup
    )
import Darcs.Patch.Rebase.Name ( RebaseName(..) )
import Darcs.Patch.Rebase.PushFixup
  ( PushFixupFn, dropFixups
  , pushFixupFLMB_FLFLMB
  , pushFixupIdMB_FLFLMB
  , pushFixupIdMB_FLIdFLFL
  )
import Darcs.Patch.RepoPatch ( RepoPatch )
import Darcs.Patch.RepoType ( RepoType(..), RebaseType(..) )
import Darcs.Patch.Show ( ShowPatchBasic(..), ShowPatchFor(..), ShowContextPatch(..) )
import Darcs.Patch.Unwind ( Unwound(..), fullUnwind )
import Darcs.Patch.Witnesses.Maybe ( Maybe2(..) )
import Darcs.Patch.Witnesses.Ordered
import Darcs.Patch.Witnesses.Sealed
import Darcs.Patch.Witnesses.Show ( Show1, Show2 )
import Darcs.Patch.Witnesses.Unsafe ( unsafeCoerceP )
import qualified Darcs.Util.Diff as D ( DiffAlgorithm )
import Darcs.Util.IsoDate ( getIsoDateTime )
import Darcs.Util.Parser ( lexString )
import Darcs.Util.Printer ( Doc, ($$), ($+$), (<+>), blueText, redText, empty, vcat )

import qualified Data.ByteString.Char8 as BC ( pack )
import Data.List ( (\\) )
import Data.List.Ordered ( nubSort )
import Data.Maybe ( fromMaybe )

data RebaseChange prim wX wY where
    RC :: FL (RebaseFixup prim) wX wY -> Named prim wY wZ -> RebaseChange prim wX wZ

instance Show2 prim => Show1 (RebaseChange prim wX)

instance Show2 prim => Show2 (RebaseChange prim)

deriving instance Show2 prim => Show (RebaseChange prim wX wY)

-- |Get hold of the 'Named' patch inside a 'RebaseChange' and wrap it in a
-- 'PatchInfoAnd'.
rcToPia :: RebaseChange prim wX wY -> Sealed2 (PatchInfoAnd ('RepoType 'NoRebase) prim)
rcToPia (RC _ toEdit) = Sealed2 (n2pia toEdit)

instance PrimPatch prim => PrimPatchBase (RebaseChange prim) where
  type PrimOf (RebaseChange prim) = prim

instance PatchDebug prim => PatchDebug (RebaseChange prim)

instance HasDeps (RebaseChange prim) where
  getdeps (RC _ toedit) = getdeps toedit

type instance PatchId (RebaseChange prim) = PatchInfo

instance Ident (RebaseChange prim) where
  ident (RC _ toedit) = ident toedit

instance Apply prim => Apply (RebaseChange prim) where
   type ApplyState (RebaseChange prim) = ApplyState prim
   apply (RC fixups toedit) = apply fixups >> apply toedit
   unapply (RC fixups toedit) = unapply toedit >> unapply fixups

instance Commute prim => Summary (RebaseChange prim) where
  conflictedEffect (RC fixups toedit) =
    case flToNamesPrims fixups of
      _names :> prims ->
        -- Report on the conflicts we would get if we unsuspended just this patch.
        -- An alternative implementation strategy would be to "force commute"
        -- prims :> toedit and report on the resulting conflicts in toedit.
        -- However this ties us to a specific RepoPatch type which isn't really
        -- needed for a simple calculation like this.
        --
        -- The rebase invariants should mean that 'fixups' (if non-empty) won't
        -- commute with 'changes' as a whole, but here we need to report each individual
        -- prim as conflicted or not, so we try to push the fixups as far through
        -- the individual prims as we can.
        --
        -- Taking the effect also means that any conflicts already present in the
        -- suspended patch won't be reported, but in general such conflicts
        -- are not supported anyway.
        case genCommuteWhatWeCanFL (commuterFLId commute) (prims :> patchcontents toedit) of
          unconflicted :> _ :> conflicted ->
            mapFL (IsC Okay) unconflicted ++ mapFL (IsC Conflicted) conflicted

instance (ShowPatchBasic prim, Invert prim, PatchListFormat prim)
  => ShowPatchBasic (RebaseChange prim) where
  showPatch ForStorage (RC fixups toedit) =
    blueText "rebase-change"
      <+> blueText "(" $$ showPatch ForStorage fixups $$ blueText ")"
      $$ showPatch ForStorage toedit
  showPatch ForDisplay p@(RC _ (NamedP n _ _)) =
    displayPatchInfo n $$ rebaseChangeContent p

rebaseChangeContent :: (ShowPatchBasic prim, Invert prim)
                   => RebaseChange prim wX wY -> Doc
rebaseChangeContent (RC fixups contents) =
  vcat (mapFL (showPatch ForDisplay) (patchcontents contents)) $+$
  if nullFL fixups
    then empty
    else redText "conflicts:" $+$ vcat (mapRL showFixup (invertFL fixups))
  where
    showFixup (PrimFixup p) = displayPatch p
    showFixup (NameFixup n) = displayPatch n

instance PrimPatch prim => ShowPatch (RebaseChange prim) where
    -- This should really just call 'description' on the ToEdit patch,
    -- but that introduces a spurious dependency on Summary (PrimOf p),
    -- because of other methods in the Named instance, so we just inline
    -- the implementation from Named here.
    description (RC _ (NamedP n _ _)) = displayPatchInfo n
    -- TODO report conflict indicating name fixups (i.e. dropped deps)
    summary p@(RC _ (NamedP _ ds _)) =
      showDependencies ShowDepsSummary ds $$ plainSummary p
    summaryFL ps =
      showDependencies ShowDepsSummary (getdepsFL ps) $$ plainSummaryFL ps
      where
        getdepsFL = nubSort . concat . mapFL getdeps
    content = rebaseChangeContent

-- TODO this is a dummy instance that does not actually show context
instance (ShowPatchBasic prim, Invert prim, PatchListFormat prim)
  => ShowContextPatch (RebaseChange prim) where
    showContextPatch f p = return $ showPatch f p

instance (ReadPatch prim, PatchListFormat prim) => ReadPatch (RebaseChange prim) where
  readPatch' = do
    lexString (BC.pack "rebase-change")
    lexString (BC.pack "(")
    Sealed fixups <- readPatch'
    lexString (BC.pack ")")
    Sealed contents <- readPatch'
    return $ Sealed $ RC fixups contents

toRebaseChanges
    :: FL (RebaseChange prim) wX wY
    -> FL (PatchInfoAndG ('RepoType 'IsRebase) (RebaseChange prim)) wX wY
toRebaseChanges = mapFL_FL n2pia

instance Commute prim => Commute (RebaseChange prim) where
  commute (RC fixups1 edit1 :> RC fixups2 edit2) = do
    fixups2' :> edit1' <- commuterIdFL commuteNamedFixup (edit1 :> fixups2)
    edit2' :> edit1'' <- commute (edit1' :> edit2)
    fixupsS :> (fixups2'' :> edit2'') :> fixups1' <-
      return $ pushThrough (fixups1 :> (fixups2' :> edit2'))
    return (RC (fixupsS +>+ fixups2'') edit2'' :> RC fixups1' edit1'')

instance PatchInspect prim => PatchInspect (RebaseChange prim) where
   listTouchedFiles (RC fixup toedit) = nubSort (listTouchedFiles fixup ++ listTouchedFiles toedit)
   hunkMatches f (RC fixup toedit) = hunkMatches f fixup || hunkMatches f toedit

-- |Split a list of rebase patches into those that will
-- have conflicts if unsuspended and those that won't.
partitionUnconflicted
    :: Commute prim
    => FL (RebaseChange prim) wX wY
    -> (FL (RebaseChange prim) :> RL (RebaseChange prim)) wX wY
partitionUnconflicted = partitionUnconflictedAcc NilRL

partitionUnconflictedAcc
  :: Commute prim
  => RL (RebaseChange prim) wX wY -> FL (RebaseChange prim) wY wZ
  -> (FL (RebaseChange prim) :> RL (RebaseChange prim)) wX wZ
partitionUnconflictedAcc right NilFL = NilFL :> right
partitionUnconflictedAcc right (p :>: ps) =
   case commuteRL (right :> p) of
     Just (p'@(RC NilFL _) :> right')
       -> case partitionUnconflictedAcc right' ps of
            left' :> right'' -> (p' :>: left') :> right''
     _ -> partitionUnconflictedAcc (right :<: p) ps

-- | A patch, together with a list of patch names that it used to depend on,
-- but were lost during the rebasing process. The UI can use this information
-- to report them to the user.
data WithDroppedDeps p wX wY =
    WithDroppedDeps {
        wddPatch :: p wX wY,
        wddDependedOn :: [PatchInfo]
    }

noDroppedDeps :: p wX wY -> WithDroppedDeps p wX wY
noDroppedDeps p = WithDroppedDeps p []

instance PrimPatchBase p => PrimPatchBase (WithDroppedDeps p) where
   type PrimOf (WithDroppedDeps p) = PrimOf p

instance Effect p => Effect (WithDroppedDeps p) where
   effect = effect . wddPatch

-- |Given a list of rebase items, try to push a new fixup as far as possible into
-- the list as possible, using both commutation and coalescing. If the fixup
-- commutes past all the 'ToEdit' patches then it is dropped entirely.
simplifyPush
  :: PrimPatch prim
  => D.DiffAlgorithm
  -> RebaseFixup prim wX wY
  -> FL (RebaseChange prim) wY wZ
  -> Sealed (FL (RebaseChange prim) wX)
simplifyPush da fixup items = dropFixups $ pushFixupChanges da (fixup :> items)

-- |Like 'simplifyPush' but for a list of fixups.
simplifyPushes
  :: PrimPatch prim
  => D.DiffAlgorithm
  -> FL (RebaseFixup prim) wX wY
  -> FL (RebaseChange prim) wY wZ
  -> Sealed (FL (RebaseChange prim) wX)
simplifyPushes _ NilFL ps = Sealed ps
simplifyPushes da (f :>: fs) ps = unseal (simplifyPush da f) (simplifyPushes da fs ps)

pushFixupChange
  :: PrimPatch prim
  => D.DiffAlgorithm
  -> PushFixupFn
       (RebaseFixup prim) (RebaseChange prim)
       (RebaseChange prim) (Maybe2 (RebaseFixup prim))
pushFixupChange da (f1 :> RC fs2 e)
  = case pushFixupFLMB_FLFLMB (pushFixupFixup da) (f1 :> fs2) of
      fs2' :> Nothing2 -> RC fs2' e :> Nothing2
      fs2' :> Just2 f1' ->
        case commuteFixupNamed (f1' :> e) of
          -- The fixup is "stuck" so just attach it here
          Nothing -> RC (fs2' +>+ f1' :>: NilFL) e :> Nothing2
          Just (e' :> f1'') -> RC fs2' e' :> Just2 f1''

pushFixupChanges
  :: PrimPatch prim
  =>  D.DiffAlgorithm
  -> PushFixupFn
       (RebaseFixup prim) (FL (RebaseChange prim))
       (FL (RebaseChange prim)) (Maybe2 (RebaseFixup prim))
pushFixupChanges da = pushFixupIdMB_FLFLMB (pushFixupChange da)

pushFixupsChange
  :: PrimPatch prim
  => D.DiffAlgorithm
  -> PushFixupFn
       (FL (RebaseFixup prim)) (RebaseChange prim)
       (RebaseChange prim) (FL (RebaseFixup prim))
pushFixupsChange da = pushFixupIdMB_FLIdFLFL (pushFixupChange da)


-- Note, this could probably be rewritten using a generalised commuteWhatWeCanFL from
-- Darcs.Patch.Permutations.
-- |@pushThrough (ps :> (qs :> te))@ tries to commute as much of @ps@ as possible through
-- both @qs@ and @te@, giving @psStuck :> (qs' :> te') :> psCommuted@.
-- Anything that can be commuted ends up in @psCommuted@ and anything that can't goes in
-- @psStuck@.
pushThrough
  :: Commute prim
  => (FL (RebaseFixup prim) :> (FL (RebaseFixup prim) :> Named prim)) wX wY
  -> (FL (RebaseFixup prim) :> (FL (RebaseFixup prim) :> Named prim) :> FL (RebaseFixup prim)) wX wY
pushThrough (NilFL :> v) = NilFL :> v :> NilFL
pushThrough ((p :>: ps) :> v) =
  case pushThrough (ps :> v) of
   psS :> v'@(qs:>te) :> ps' ->
     fromMaybe ((p :>: psS) :> v' :> ps') $ do
       psS' :> p' <- commuteFL (p :> psS)
       qs' :> p'' <- commuteFL (p' :> qs)
       te' :> p''' <- commuteFixupNamed (p'' :> te)
       return (psS' :> (qs' :> te') :> (p''' :>: ps'))

type WDDNamed p = WithDroppedDeps (Named p)

mergerIdWDD :: MergeFn p1 p2 -> MergeFn p1 (WithDroppedDeps p2)
mergerIdWDD merger (p1 :\/: WithDroppedDeps p2 deps) =
   case merger (p1 :\/: p2) of
     p2' :/\: p1' -> WithDroppedDeps p2' deps :/\: p1'

commuterIdWDD :: CommuteFn p q -> CommuteFn p (WithDroppedDeps q)
commuterIdWDD commuter (p :> WithDroppedDeps q deps)
  = do -- no need to worry about names, because by definition a dropped dep
       -- is a name we no longer have
       -- TODO consistency checking?
       -- TODO consider inverse commutes, e.g. what happens if we wanted to
       -- commute (WithDroppedDeps ... [n] :> AddName n)?
       q' :> p' <- commuter (p :> q)
       return (WithDroppedDeps q' deps :> p')

-- |Forcibly commute a 'RebaseName' with a patch, dropping any dependencies
-- if necessary and recording them in the patch
forceCommuteName :: (RebaseName :> WDDNamed p) wX wY -> (WDDNamed p :> RebaseName) wX wY
forceCommuteName (AddName an :> WithDroppedDeps (NamedP pn deps body) ddeps)
  | an == pn = error "impossible case"
  | otherwise =
      WithDroppedDeps
        (NamedP pn (deps \\ [an]) (unsafeCoerceP body))
        (if an `elem` deps then an : ddeps else ddeps)
      :>
      AddName an
forceCommuteName (DelName dn :> p@(WithDroppedDeps (NamedP pn deps _body) _ddeps))
  | dn == pn = error "impossible case"
  | dn `elem` deps = error "impossible case"
  | otherwise = unsafeCoerceP p :> DelName dn
forceCommuteName (Rename old new :> WithDroppedDeps (NamedP pn deps body) ddeps)
  | old == pn = error "impossible case"
  | new == pn = error "impossible case"
  | old `elem` deps = error "impossible case"
  | otherwise =
      let newdeps = map (\dep -> if new == dep then old else dep) deps
      in WithDroppedDeps (NamedP pn newdeps (unsafeCoerceP body)) ddeps :> Rename old new

forceCommutePrim :: RepoPatch p
                 => (PrimOf p :> WDDNamed p) wX wY
                 -> (WDDNamed p :> FL (PrimOf p)) wX wY
forceCommutePrim (p :> wq) =
    -- rp and irp are not inverses for RepoPatchV3, only their effects are inverse
    let rp = fromAnonymousPrim p
        irp = fromAnonymousPrim (invert p)
    in case mergerIdWDD (mergerIdNamed selfMerger) (irp :\/: wq) of
        wq' :/\: irp' -> prefixWith (rp :>: irp :>: NilFL) wq' :> invert (effect irp')
    where
      -- TODO [V3INTEGRATION]:
      -- This is a hack to adapt forceCommutePrim to the stricter assumptions
      -- made by RepoPatchV3, for which resolveConflicts expects that we can
      -- find each patch we conflict with somewhere in the context.
      -- Force-commuting the fixups with the patch to be edited violates that
      -- assumption. It works for RepoPatchV1/2 because their conflictors are
      -- self-contained i.e. they contain the transitive set of conflicts in
      -- their representation, which is no longer true for RepoPatchV3.
      -- To restore the assumption for RepoPatchV3 we prefix the patches
      -- contained in the 'Named' patch with (rp;irp). The conflictor wq' can
      -- now refer to irp, and the effect of rp will cancel with that of irp
      -- on unsuspend.
      prefixWith xs (WithDroppedDeps (NamedP i ds ps) dds) =
          WithDroppedDeps (NamedP i ds (xs +>+ ps)) dds

forceCommutes :: RepoPatch p
              => (FL (RebaseFixup (PrimOf p)) :> WDDNamed p) wX wY
              -> (WDDNamed p :> FL (RebaseFixup (PrimOf p))) wX wY
forceCommutes (NilFL :> q) = q :> NilFL
forceCommutes ((NameFixup n :>: ps) :> q) =
    case forceCommutes (ps :> q) of
        q' :> ps' ->
            case forceCommuteName (n :> q') of
                q'' :> n' -> q'' :> (NameFixup n' :>: ps')
forceCommutes ((PrimFixup p :>: ps) :> q) =
    case forceCommutes (ps :> q) of
        q' :> ps' ->
            case forceCommutePrim (p :> q') of
                qs'' :> p' -> qs'' :> (mapFL_FL PrimFixup p' +>+ ps')

fromPrimNamed :: FromPrim p => Named (PrimOf p) wX wY -> Named p wX wY
fromPrimNamed (NamedP n deps ps) = NamedP n deps (fromPrims n ps)

-- |Turn a selected rebase patch back into a patch we can apply to
-- the main repository, together with residual fixups that need
-- to go back into the rebase state (unless the rebase is now finished).
-- Any fixups associated with the patch will turn into conflicts.
extractRebaseChange
  :: forall p wX wY
   . RepoPatch p
  => D.DiffAlgorithm
  -> FL (RebaseChange (PrimOf p)) wX wY
  -> (FL (WDDNamed p) :> FL (RebaseFixup (PrimOf p))) wX wY
extractRebaseChange da rcs = go (NilFL :> rcs)
  where
    go
      :: forall wA wB
       . (FL (RebaseFixup (PrimOf p)) :> FL (RebaseChange (PrimOf p))) wA wB
      -> (FL (WDDNamed p) :> FL (RebaseFixup (PrimOf p))) wA wB
    go (fixupsIn :> NilFL) = NilFL :> fixupsIn
    go (fixupsIn :> rc :>: rest) =
      -- First simplify any fixups coming from previous extract operations.
      -- Note that it's important to start at the front of the list so that
      -- we can do this, as it minimises the conflicts we end up with.
      case pushFixupsChange da (fixupsIn :> rc) of
        -- Now use 'fromPrimNamed' to change the toedit patch from
        -- Named (PrimOf p) that we store in the rebase to Named p
        -- that we store in the repository. Then, wrap it in WithDroppedDeps
        -- so we can track any explicit dependencies that were lost, and
        -- finally force-commute the fixups with this and any other patches we are
        -- unsuspending.
        RC fixups toedit :> fixupsOut2 ->
          case forceCommutes (fixups :> WithDroppedDeps (fromPrimNamed toedit) []) of
            toedit' :> fixupsOut1 ->
              case go (fixupsOut1 +>+ fixupsOut2 :> rest) of
                toedits' :> fixupsOut -> toedit' :>: toedits' :> fixupsOut

-- signature to be compatible with extractRebaseChange
-- | Like 'extractRebaseChange', but any fixups are "reified" into a separate patch.
reifyRebaseChange
  :: FromPrim p
  => String
  -> FL (RebaseChange (PrimOf p)) wX wY
  -> IO ((FL (WDDNamed p) :> FL (RebaseFixup (PrimOf p))) wX wY)
reifyRebaseChange author rs = do
    res <- concatFL <$> mapFL_FL_M reifyOne rs
    return (res :> NilFL)
  where
    reifyOne :: FromPrim p => RebaseChange (PrimOf p) wA wB -> IO (FL (WDDNamed p) wA wB)
    reifyOne (RC fixups toedit) =
      case flToNamesPrims fixups of
        names :> NilFL ->
          return $
            mapFL_FL (noDroppedDeps . mkDummy) names +>+
            noDroppedDeps (fromPrimNamed toedit) :>:
            NilFL
        names :> prims -> do
          n <- mkReified author prims
          return $
            mapFL_FL (noDroppedDeps . mkDummy) names +>+ noDroppedDeps n :>:
            noDroppedDeps (fromPrimNamed toedit) :>:
            NilFL

mkReified :: FromPrim p => String -> FL (PrimOf p) wX wY -> IO (Named p wX wY)
mkReified author ps = do
     let name = "Reified fixup patch"
     let desc = []
     date <- getIsoDateTime
     info <- patchinfo date name author desc
     return $ infopatch info ps

mkDummy :: FromPrim p => RebaseName wX wY -> Named p wX wY
mkDummy (AddName pi) = infopatch pi (unsafeCoerceP NilFL)
mkDummy (DelName _) = error "internal error: can't make a dummy patch from a delete"
mkDummy (Rename _ _) = error "internal error: can't make a dummy patch from a rename"

instance IsHunk (RebaseChange prim) where
    -- RebaseChange is a compound patch, so it doesn't really make sense to
    -- ask whether it's a hunk. TODO: get rid of the need for this.
    isHunk _ = Nothing

instance PatchListFormat (RebaseChange prim)

addNamedToRebase
  :: RepoPatch p
  => D.DiffAlgorithm
  -> Named p wX wY
  -> FL (RebaseChange (PrimOf p)) wY wZ
  -> Sealed (FL (RebaseChange (PrimOf p)) wX)
addNamedToRebase da named@(NamedP n deps _) =
  case fullUnwind named of
    Unwound before underlying after ->
      unseal (simplifyPushes da (mapFL_FL PrimFixup before)) .
      mapSeal ((RC NilFL (NamedP n deps underlying) :>:)) .
      simplifyPushes da (mapFL_FL PrimFixup (reverseRL after))