module Darcs.UI.PatchHeader
    ( getLog
    , getAuthor
    , editLog
    , updatePatchHeader, AskAboutDeps(..)
    , PatchHeaderConfig
    , patchHeaderConfig
    , HijackT, HijackOptions(..)
    , runHijackT
    ) where

import Darcs.Prelude

import Darcs.Patch ( PrimOf, RepoPatch, summaryFL )
import Darcs.Patch.Apply ( ApplyState )
import Darcs.Patch.Info
    ( PatchInfo
    , patchinfo
    , piAuthor
    , piDateString
    , piLog
    , piName
    )
import Darcs.Patch.Named
    ( Named
    , adddeps
    , getdeps
    , infopatch
    , patch2patchinfo
    , patchcontents
    , setinfo
    )
import Darcs.Patch.PatchInfoAnd ( PatchInfoAnd, n2pia )
import Darcs.Patch.Prim ( canonizeFL )

import Darcs.Patch.Witnesses.Ordered ( FL(..), RL(..), (+>+) )

import Darcs.Util.Lock ( readTextFile, writeTextFile )

import Darcs.UI.External ( editFile )
import Darcs.UI.Flags ( getEasyAuthor, promptAuthor, getDate )
import Darcs.UI.Options ( Config, (?) )
import qualified Darcs.UI.Options.All as O
import qualified Darcs.UI.SelectChanges as S ( PatchSelectionOptions(..) )
import Darcs.UI.SelectChanges ( askAboutDepends )

import qualified Darcs.Util.Diff as D ( DiffAlgorithm )
import Darcs.Util.English ( capitalize )
import Darcs.Util.Global ( darcsLastMessage )
import Darcs.Util.Path ( FilePathLike, toFilePath )
import Darcs.Util.Prompt ( PromptConfig(..), askUser, promptChar, promptYorn )
import Darcs.Util.Printer ( Doc, text, ($+$), vcat, prefixLines, renderString )
import qualified Darcs.Util.Ratified as Ratified ( hGetContents )

import Darcs.Util.Tree ( Tree )

import Control.Exception ( catch, IOException )
import Control.Monad ( when, void )
import Control.Monad.Trans              ( liftIO )
import Control.Monad.Trans.State.Strict ( StateT(..), evalStateT, get, put  )
import Data.List ( isPrefixOf, stripPrefix )
import Data.Maybe ( fromMaybe, isJust )
import System.Exit ( exitSuccess )
import System.IO ( stdin )

data PName = FlagPatchName String | PriorPatchName String | NoPatchName

-- | Options for how to deal with the situation where we are somehow
--   modifying a patch that is not our own
data HijackOptions = IgnoreHijack                  -- ^ accept all hijack requests
                   | RequestHijackPermission       -- ^ prompt once, accepting subsequent hijacks if yes
                   | AlwaysRequestHijackPermission -- ^ always prompt

-- | Transformer for interactions with a hijack warning state that we
--   need to thread through
type HijackT = StateT HijackOptions

-- | Get the patch name and long description from one of
--
--  * the configuration (flags, defaults, hard-coded)
--
--  * an existing log file
--
--  * stdin (e.g. a pipe)
--
--  * a text editor
--
-- It ensures the patch name is not empty nor starts with the prefix TAG.
--
-- The last result component is a possible path to a temporary file that should be removed later.
getLog :: Maybe String                          -- ^ patchname option
       -> Bool                                  -- ^ pipe option
       -> O.Logfile                             -- ^ logfile option
       -> Maybe O.AskLongComment                -- ^ askLongComment option
       -> Maybe (String, [String])              -- ^ possibly an existing patch name and long description
       -> Doc                                   -- ^ summary of changes to record
       -> IO (String, [String], Maybe String)   -- ^ patch name, long description and possibly the path
                                                --   to the temporary file that should be removed later
getLog m_name has_pipe log_file ask_long m_old chs =
  restoreTagPrefix <$> go has_pipe log_file ask_long
 where
  go True _ _ = do
      p <- case patchname_specified of
             FlagPatchName p  -> check_badname p >> return p
             PriorPatchName p -> return p
             NoPatchName      -> prompt_patchname False
      putStrLn "What is the log?"
      thelog <- lines `fmap` Ratified.hGetContents stdin
      return (p, thelog, Nothing)
  go _ (O.Logfile { O._logfile = Just f }) _ = do
      mlp <- readTextFile f `catch` (\(_ :: IOException) -> return [])
      firstname <- case (patchname_specified, mlp) of
                     (FlagPatchName  p, []) -> check_badname p >> return p
                     (_, p:_)               -> if is_badname p
                                                 then prompt_patchname True
                                                 else return p -- logfile trumps prior!
                     (PriorPatchName p, []) -> return p
                     (NoPatchName, [])      -> prompt_patchname True
      append_info f firstname
      when (ask_long == Just O.YesEditLongComment) (void $ editFile f)
      (name, thelog) <- read_long_comment f firstname
      return (name, thelog, if O._rmlogfile log_file then Just $ toFilePath f else Nothing)
  go _ _ (Just O.YesEditLongComment) =
      case patchname_specified of
          FlagPatchName  p  -> get_log_using_editor p
          PriorPatchName p  -> get_log_using_editor p
          NoPatchName       -> get_log_using_editor ""
  go _ _ (Just O.NoEditLongComment) =
      case patchname_specified of
          FlagPatchName  p  -> check_badname p >> return (p, default_log, Nothing) -- record (or amend) -m
          PriorPatchName p  -> return (p, default_log, Nothing) -- amend
          NoPatchName       -> do p <- prompt_patchname True -- record
                                  return (p, [], Nothing)
  go _ _ (Just O.PromptLongComment) =
      case patchname_specified of
          FlagPatchName p   -> check_badname p >> prompt_long_comment p -- record (or amend) -m
          PriorPatchName p  -> prompt_long_comment p
          NoPatchName       -> prompt_patchname True >>= prompt_long_comment
  go _ _ Nothing =
      case patchname_specified of
          FlagPatchName  p  -> check_badname p >> return (p, default_log, Nothing)  -- record (or amend) -m
          PriorPatchName "" -> get_log_using_editor ""
          PriorPatchName p  -> return (p, default_log, Nothing)
          NoPatchName       -> get_log_using_editor ""

  tagPrefix = "TAG "

  hasTagPrefix name = tagPrefix `isPrefixOf` name

  restoreTagPrefix (name, log, file)
    | Just (old_name, _) <- m_old
    , hasTagPrefix old_name = (tagPrefix ++ name, log, file)
  restoreTagPrefix args = args

  stripTagPrefix name = fromMaybe name $ stripPrefix tagPrefix name

  patchname_specified =
    case (m_name, m_old) of
      (Just name, _)              -> FlagPatchName name
      (Nothing,   Just (name, _)) -> PriorPatchName (stripTagPrefix name)
      (Nothing,   Nothing)        -> NoPatchName

  default_log = case m_old of
                  Nothing    -> []
                  Just (_,l) -> l

  check_badname = maybe (return ()) fail . just_a_badname

  prompt_patchname retry = do
      n <- askUser "What is the patch name? "
      maybe (return n) prompt_again $ just_a_badname n
    where
      prompt_again msg = do
        putStrLn msg
        if retry then prompt_patchname retry else fail "Bad patch name!"

  just_a_badname n =
    if null n then
      Just "Error: The patch name must not be empty!"
    else if hasTagPrefix n then
      Just "Error: The patch name must not start with \"TAG \"!"
    else
      Nothing

  is_badname = isJust . just_a_badname

  prompt_long_comment oldname =
    do let verb = case m_old of Nothing -> "add a"; Just _ -> "edit the"
       y <- promptYorn $ "Do you want to "++verb++" long comment?"
       if y then get_log_using_editor oldname
            else return (oldname, default_log, Nothing)

  get_log_using_editor p =
                       do let logf = darcsLastMessage
                          writeTextFile logf $ unlines $ p : default_log
                          append_info logf p
                          _ <- editFile logf
                          (name,long) <- read_long_comment logf p
                          check_badname name
                          return (name,long,Just logf)

  read_long_comment :: FilePathLike p => p -> String -> IO (String, [String])
  read_long_comment f oldname =
      do t <- readTextFile f
         let filter_out_info = filter (not.("#" `isPrefixOf`))
         case reverse $ dropWhile null $ reverse $ filter_out_info t of
            []     -> return (oldname, [])
            (n:ls) -> do
                check_badname n
                return (n, ls)

  append_info f oldname = do
    fc <- readTextFile f
    writeTextFile f $ renderString
      $ vcat (map text $ if null fc then [oldname] else fc)
      $+$ vcat
        [ text "# Please enter the patch name in the first line, and"
        , text "# optionally, a long description in the following lines."
        , text "#"
        , text "# Lines starting with '#' will be ignored."
        , text "#"
        , text "#"
        , text "# Summary of selected changes:"
        , text "#"
        , prefixLines (text "#") chs
        ]

editLog :: Named prim wX wY -> IO (Named prim wX wY)
editLog p = do
  let pi = patch2patchinfo p
  (name, log, _) <-
    getLog Nothing False (O.Logfile Nothing False)
      (Just O.YesEditLongComment) (Just (piName pi, piLog pi)) mempty
  pi' <- patchinfo (piDateString pi) name (piAuthor pi) log
  return $ setinfo pi' p

-- | Specify whether to ask about dependencies with respect to a particular
-- 'PatchSet', or not
data AskAboutDeps p wX where
  AskAboutDeps :: (RL (PatchInfoAnd p) w wX) -> AskAboutDeps p wX
  NoAskAboutDeps :: AskAboutDeps p wX

-- | Run a job that involves a hijack confirmation prompt.
--
--   See 'RequestHijackPermission' for initial values
runHijackT :: Monad m => HijackOptions -> HijackT m a -> m a
runHijackT = flip evalStateT

data PatchHeaderConfig = PatchHeaderConfig
  { diffAlgorithm :: D.DiffAlgorithm
  , keepDate :: Bool
  , selectAuthor :: Bool
  , author :: Maybe String
  , patchname :: Maybe String
  , askLongComment :: Maybe O.AskLongComment
  }

patchHeaderConfig :: Config -> PatchHeaderConfig
patchHeaderConfig cfg = PatchHeaderConfig
  { diffAlgorithm   = O.diffAlgorithm ? cfg
  , keepDate        = O.keepDate ? cfg
  , selectAuthor    = O.selectAuthor ? cfg
  , author          = O.author ? cfg
  , patchname       = O.patchname ? cfg
  , askLongComment  = O.askLongComment ? cfg
  }

-- | Update the metadata for a patch.
--   This potentially involves a bit of interactivity, so we may return @Nothing@
--   if there is cause to abort what we're doing along the way
updatePatchHeader :: forall p wX wY wZ . (RepoPatch p, ApplyState p ~ Tree)
                  => String -- ^ verb: command name
                  -> AskAboutDeps p wX
                  -> S.PatchSelectionOptions
                  -> PatchHeaderConfig
                  -> Named (PrimOf p) wX wY
                  -- ^ patch to edit, must be conflict-free as conflicts can't
                  -- be preserved when changing the identity of a patch. If
                  -- necessary this can be achieved by calling @fmapFL_Named
                  -- effect@ on an @Named p@ first, but some callers might
                  -- already have @Named (PrimOf p)@ available.
                  -> FL (PrimOf p) wY wZ -- ^new primitives to add
                  -> HijackT IO (Maybe String, PatchInfoAnd p wX wZ)
updatePatchHeader verb ask_deps pSelOpts PatchHeaderConfig{..} oldp chs = do

    let newchs = canonizeFL diffAlgorithm (patchcontents oldp +>+ chs)

    let old_pdeps = getdeps oldp
    newdeps <-
        case ask_deps of
           AskAboutDeps patches ->
              liftIO $ askAboutDepends patches newchs pSelOpts old_pdeps
           NoAskAboutDeps -> return old_pdeps

    let old_pinf = patch2patchinfo oldp
        prior    = (piName old_pinf, piLog old_pinf)
    date <- if keepDate then return (piDateString old_pinf) else liftIO $ getDate False
    new_author <- getAuthor verb selectAuthor author old_pinf
    liftIO $ do
        (new_name, new_log, mlogf) <- getLog
            patchname False (O.Logfile Nothing False) askLongComment (Just prior) (summaryFL chs)
        new_pinf <- patchinfo date new_name new_author new_log
        let newp = n2pia (adddeps (infopatch new_pinf newchs) newdeps)
        return (mlogf, newp)


-- | @getAuthor@ tries to return the updated author for the patch.
--   There are two different scenarios:
--
--   * [explicit] Either we want to override the patch author, be it by
--     prompting the user (@select@) or having them pass it in from
--     the UI (@new_author@), or
--
--   * [implicit] We want to keep the original author, in which case we
--     also double-check that we are not inadvertently \"hijacking\"
--     somebody else's patch (if the patch author is not the same as the
--     repository author, we give them a chance to abort the whole
--     operation)
getAuthor :: String          -- ^ verb:   command name
          -> Bool            -- ^ select: prompt for new auhor
          -> Maybe String    -- ^ new author: explict new author
          -> PatchInfo       -- ^ patch to update
          -> HijackT IO String
getAuthor _ True  _ _  = do
    auth <- liftIO $ promptAuthor False True
    return auth
getAuthor _    False (Just new) _   =
    return new
getAuthor verb False Nothing pinfo = do
    whitelist <- liftIO $ getEasyAuthor
    hj <- get
    if orig `elem` whitelist || canIgnore hj
        then allowHijack
        else do
            hijackResp <- liftIO $ askAboutHijack hj
            case hijackResp of
                'y' -> allowHijack
                'a' -> put IgnoreHijack >> allowHijack
                _   -> liftIO exitSuccess
  where
    askAboutHijack hj = promptChar (PromptConfig msg opts [] Nothing [])
       where
         msg  = "You're not " ++ orig ++"! " ++ capitalize verb ++ " anyway? "
         opts = case hj of
             AlwaysRequestHijackPermission -> "yn"
             _ -> "yna"
    canIgnore IgnoreHijack                  = True
    canIgnore RequestHijackPermission       = False
    canIgnore AlwaysRequestHijackPermission = False
    allowHijack = return orig
    orig = piAuthor pinfo
