{-# LANGUAGE OverloadedStrings #-}
module Darcs.UI.Commands.ShowDependencies ( showDeps ) where

import Darcs.Prelude

import qualified Data.Map.Strict as M
import Data.Maybe( fromJust, fromMaybe )
import qualified Data.Set as S

import Darcs.Repository ( RepoJob(..), readPatches, withRepositoryLocation )

import Darcs.UI.Flags ( DarcsFlag, getRepourl, useCache )
import Darcs.UI.Options ( oid, (?) )
import qualified Darcs.UI.Options.All as O
import Darcs.UI.Commands ( DarcsCommand(..), nodefaults, findRepository, withStdOpts )
import Darcs.UI.Commands.Util ( matchRange )
import Darcs.UI.Completion ( noArgs )

import Darcs.Util.Hash ( sha1short, showAsHex )
import Darcs.Util.Path ( AbsolutePath )
import Darcs.Util.Printer
    ( Doc
    , (<+>)
    , ($+$)
    , formatText
    , formatWords
    , hsep
    , prefixLines
    , putDocLn
    , quoted
    , renderString
    , text
    , vcat
    )
import Darcs.Util.Progress ( beginTedious, endTedious, progress, tediousSize )

import Darcs.Patch.Commute ( Commute, commuteFL )
import Darcs.Patch.Ident ( PatchId, Ident(..) )
import Darcs.Patch.Info ( PatchInfo, piName, makePatchname )
import Darcs.Patch.Witnesses.Ordered
    ( (:>)(..)
    , FL(..)
    , RL(..)
    , reverseFL
    , lengthFL
    )
import Darcs.Patch.Witnesses.Sealed ( Sealed2(..) )

showDepsDescription :: String
showDepsDescription = "Generate the graph of dependencies."

showDepsHelp :: Doc
showDepsHelp =
  formatWords
    [ "This command creates a graph of the dependencies between patches."
    , "The output format is the Dot Language, see"
    , "https://www.graphviz.org/doc/info/lang.html. The resulting graph"
    , "is transitively reduced, in other words,"
    , "it contains only the direct dependencies, not the indirect ones."
    ]
  $+$ formatWords
    [ "By default all patches in your repository are considered. You can"
    , "limit this to a range of patches using patch matching options, see"
    , "`darcs help patterns` and the options avaiable for this command."
    , "For instance, to visualize the dependencies between all patches"
    , "since the last tag, do:"
    ]
  $+$ "    darcs show dependencies --from-tag=. | dot -Tpdf -o FILE.pdf"
  $+$ formatWords
    [ "This command can take a very(!) long time to compute its result,"
    , "depending on the number of patches in the selected range. For N"
    , "patches it needs to do on the order of N^3 commutations in the"
    , "worst case."
    ]

showDeps :: DarcsCommand
showDeps = DarcsCommand
    { commandProgramName = "darcs"
    , commandName = "dependencies"
    , commandHelp = showDepsHelp
    , commandDescription = showDepsDescription
    , commandExtraArgs = 0
    , commandExtraArgHelp = []
    , commandCommand = depsCmd
    , commandPrereq = findRepository
    , commandCompleteArgs = noArgs
    , commandArgdefaults = nodefaults
    , commandOptions = showDepsOpts
    }
  where
    showDepsBasicOpts = O.matchRange
    showDepsOpts = showDepsBasicOpts `withStdOpts` oid

progressKey :: String
progressKey = "Determining dependencies"

depsCmd :: (AbsolutePath, AbsolutePath) -> [DarcsFlag] -> [String] -> IO ()
depsCmd _ opts _ = do
    let repodir = fromMaybe "." (getRepourl opts)
    withRepositoryLocation (useCache ? opts) repodir $ RepoJob $ \repo -> do
        Sealed2 range <- matchRange (O.matchRange ? opts) <$> readPatches repo
        beginTedious progressKey
        tediousSize progressKey (lengthFL range)
        putDocLn $ renderDepsGraphAsDot $ depsGraph $ reverseFL range
        endTedious progressKey

-- | A 'M.Map' from 'PatchId's to 'Deps'.
type DepsGraph p = M.Map (PatchId p) (Deps p)

-- | A pair of (direct, indirect) dependencies. For the result we need only the
-- direct dependencies. We store the indirect ones as an optimization to avoid
-- doing commutes for which we already know that they cannot succeed. Note that
-- the two sets are always disjoint.
type Deps p = (S.Set (PatchId p), S.Set (PatchId p))

-- | Determine the 'DepsGraph' of an 'RL' of patches.
depsGraph :: forall p wX wY. (Commute p, Ident p) => RL p wX wY -> DepsGraph p
depsGraph NilRL = M.empty
depsGraph (ps :<: p) =
  M.insert (ident p) (foldDeps ps (p :>: NilFL) NilFL (S.empty, S.empty)) m
  where
    -- First recurse on the context. The result now has all the 'Deps' for
    -- all patches preceding p.
    m = depsGraph ps
    -- Lookup all (direct and indirect) dependencies of a patch in a given
    -- 'DepsGraph'
    allDeps j = uncurry S.union . fromJust . M.lookup j
    -- Add all (direct and indirect) dependencies of a patch to a given set
    addDeps j = S.insert j . S.union (allDeps j m)
    -- Add direct and indirect dependencies of a patch, assuming that the
    -- graph has already been constructed for all patches in the context.
    foldDeps :: RL p wA wB -> FL p wB wC -> FL p wC wD -> Deps p -> Deps p
    foldDeps NilRL _ _ acc = progress progressKey acc
    foldDeps (qs :<: q) p_and_deps non_deps acc@(direct, indirect)
      -- If we already know we indirectly depend on q, then there is
      -- nothing left to do. Note that (j `S.member` direct) is impossible.
      | j `S.member` indirect = foldDeps qs (q :>: p_and_deps) non_deps acc
      -- If q commutes past p_and_deps then we don't depend on it
      | Just (p_and_deps' :> q') <- commuteFL (q :> p_and_deps) =
        foldDeps qs p_and_deps' (q' :>: non_deps) acc
      -- We have a new dependency which must be a direct one, so add it to
      -- 'direct' and all its dependencies to 'indirect'. The invariant that
      -- direct and indirect are disjoint is maintained because neither the
      -- direct nor indirect deps of a patch contain its own 'PatchId'.
      | otherwise =
        foldDeps qs (q :>: p_and_deps) non_deps (S.insert j direct, addDeps j indirect)
      where
        j = ident q

-- | Render a 'DepsGraph' in the Dot Language format. This function
-- considers only the direct dependencies.
renderDepsGraphAsDot :: M.Map PatchInfo (S.Set PatchInfo, S.Set PatchInfo) -> Doc
renderDepsGraphAsDot g = vcat ["digraph {", indent body, "}"]
  where
    indent = prefixLines ("  ")
    body = vcat
      [ "graph [rankdir=LR];"
      , "node [imagescale=true];"
      , vcat (map showNode (map fst pairs))
      , vcat (map showEdges pairs)
      ]
    pairs = M.toList $ M.map fst g
    showEdges (i, ds)
      | S.null ds = mempty
      | otherwise =
          hsep [showID i, "->", "{" <> hsep (map showID (S.toList ds)) <> "}"]
    showNode i = showID i <+> "[label=" <> showLabel i <> "]"
    showID = quoted . showAsHex . sha1short . makePatchname
    showLabel i = text $ show $ renderString $ formatText 20 [piName i]
