%  Copyright (C) 2002-2004 David Roundy
%  Copyright (C) 2005 Juliusz Chroboczek
%
%  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.

\begin{code}
module Repository ( Repository, maybeIdentifyRepository, identifyRepository,
                    findRepository, amInRepository, slurp_pending,
                    slurp_recorded, slurp_recorded_and_unrecorded,
                    get_unrecorded, read_repo, sync_repo, absolute_dir,
                    prefsUrl,
                    read_pending, with_new_pending, add_to_pending,
                    withRepoLock,
#ifdef HAVEWX
                    takeRepoGuiLock, releaseRepoGuiLock, withRepoGuiLockCanFail,
#endif
                    PatchToken, patchTokenToPatchFile,
                    writePatch, updateInventory, unrevertUrl,
                    applyToPristine, patchSetToPatches,
                    PatchSet
                  ) where
import External ( fetchFilePS, Cachable( Cachable ) )
import Pristine ( Pristine, identifyPristine, nopristine, applyPristine,
                )
import RepoFormat ( RepoFormat, identifyRepoFormat,
                    write_problem, read_problem )
import Directory ( doesDirectoryExist, setCurrentDirectory )
import Monad ( liftM, when )
import Maybe ( catMaybes )
import SlurpDirectory ( Slurpy, slurp_unboring, co_slurp, slurp_has )
import DarcsRepo ( seekRepo, youNeedToBeInRepo )
import qualified DarcsRepo
import qualified GitRepo
import DarcsFlags ( DarcsFlag(AnyOrder, Boring, LookForAdds, Verbose,
                              WorkDir, UMask) )
import PatchInfo ( PatchInfo )
import Patch ( Patch, flatten, join_patches, reorder_and_coalesce,
               apply_to_slurpy, gzReadPatchFileLazily )
import Diff ( smart_diff )
import PatchSet ( PatchSet )
import Workaround ( getCurrentDirectory )
import DarcsUtils ( catchall, withCurrentDirectory, withUMask )
import RepoPrefs ( darcsdir_filter, boring_file_filter, filetype_function )
import Lock ( withLock )
#ifdef HAVEWX
import Lock ( takeLock, releaseLock, withLockCanFail )
#endif

#include "impossible.h"

data Repository = Repo !String !RepoFormat !RepoType

data RepoType = DarcsRepository !Pristine | GitRepository

maybeIdentifyRepository :: String -> IO (Either String Repository)
maybeIdentifyRepository "." =
    do darcs <- doesDirectoryExist "_darcs"
       git <- doesDirectoryExist ".git"
       rf <- identifyRepoFormat "."
       here <- absolute_dir "."
       case read_problem rf of
         Just err -> return $ Left err
         Nothing ->
           case (darcs, git) of
           (False, False) -> return (Left "Not a repository")
           (True, _)->
               do pris <- identifyPristine
                  return $ Right $ Repo here rf (DarcsRepository pris)
           (False, True) -> return (Right $ Repo here rf GitRepository)
maybeIdentifyRepository url' = do
    url <- absolute_dir url'
    darcs <- doesRemoteFileExist (url++"/_darcs/inventory")
    git <- doesRemoteFileExist (url++"/.git/HEAD")
    rf <- identifyRepoFormat url
    case read_problem rf of
        Just err -> return $ Left err
        Nothing ->
            case (darcs, git) of
            (Left s, Left _) ->
                return (Left $ "Not a repository: " ++ url' ++
                        " (" ++ s ++")")
            (Right True, _)->
                return $ Right $ Repo url rf (DarcsRepository nopristine)
            (Left _, Right True) -> return (Right $ Repo url rf GitRepository)
            _ -> impossible
    where drfe x = fetchFilePS x Cachable >> return True
          doesRemoteFileExist x = (liftM Right) (drfe x) `catch`
                                      (\e -> return (Left (show e)))

identifyRepository :: String -> IO Repository
identifyRepository url =
    do er <- maybeIdentifyRepository url
       case er of
           Left s -> fail s
           Right r -> return r

isRight :: Either a b -> Bool
isRight (Right _) = True
isRight _         = False

currentDirIsRepository :: IO Bool
currentDirIsRepository = isRight `liftM` maybeIdentifyRepository "."

amInRepository :: [DarcsFlag] -> IO (Either String FilePath)
amInRepository (WorkDir d:_) =
    do setCurrentDirectory d `catchall` (fail $ "can't set directory to "++d)
       amInRepository []
amInRepository (_:fs) = amInRepository fs
amInRepository [] = 
    seekRepo currentDirIsRepository (Left youNeedToBeInRepo)

findRepository :: [DarcsFlag] -> IO (Either String FilePath)
findRepository (WorkDir d:_) =
    do setCurrentDirectory d `catchall` (fail $ "can't set directory to "++d)
       findRepository []
findRepository (_:fs) = findRepository fs
findRepository [] = 
    seekRepo currentDirIsRepository (Right "")

slurp_pending :: Repository -> IO Slurpy
slurp_pending repo = do
  cur <- slurp_recorded repo
  mbpend <- read_pending repo
  case mbpend of
    Just pend ->
      case apply_to_slurpy pend cur of
        Just pendcur -> return pendcur
        Nothing -> do putStrLn "Yikes, pending has conflicts!"
                      return cur
    Nothing -> return cur

slurp_recorded :: Repository -> IO Slurpy
slurp_recorded (Repo r _ (DarcsRepository pristine)) =
    withCurrentDirectory r $ DarcsRepo.surely_slurp_Pristine pristine
slurp_recorded (Repo r  _ GitRepository) = GitRepo.slurpHead r

slurp_recorded_and_unrecorded :: Repository -> IO (Slurpy, Slurpy)
slurp_recorded_and_unrecorded (Repo r _ (DarcsRepository _)) =
    DarcsRepo.slurp_recorded_and_unrecorded r
slurp_recorded_and_unrecorded repo@(Repo r _ _) = do
  cur <- slurp_recorded repo
  mbpend <- read_pending repo
  withCurrentDirectory r $
    case mbpend of
    Just pend ->
      case apply_to_slurpy pend cur of
      Nothing -> fail "Yikes, pending has conflicts!"
      Just pendslurp -> do unrec <- co_slurp pendslurp "."
                           return (cur, unrec)
    Nothing -> do unrec <- co_slurp cur "."
                  return (cur, unrec)

pendingName :: RepoType -> String
pendingName (DarcsRepository _) = "_darcs/patches/pending"
pendingName GitRepository = ".git/darcs_pending"

read_pending :: Repository -> IO (Maybe Patch)
read_pending (Repo r _ tp) =
    withCurrentDirectory r (DarcsRepo.read_pending (pendingName tp))

add_to_pending :: Repository -> Patch -> IO ()
add_to_pending repo p =
    do pend <- read_pending repo
       let pnew = case pend of Nothing -> p
                               Just pold -> join_patches [pold, p]
       with_new_pending repo pnew (return ())

with_new_pending :: Repository -> Patch -> IO () -> IO ()
with_new_pending repo@(Repo r _ tp) p job =
    withCurrentDirectory r $
    DarcsRepo.with_new_pending (pendingName tp) p $ job >> slurp_recorded repo

unempty :: [a] -> Maybe [a]
unempty [] = Nothing
unempty l = Just l

get_unrecorded :: Repository -> [DarcsFlag] -> IO (Maybe Patch)
get_unrecorded repository@(Repo r _ _) opts =
  withCurrentDirectory r $ do
    cur <- slurp_pending repository
    work <- if LookForAdds `elem` opts
            then do nboring <- if Boring `elem` opts
                               then return $ darcsdir_filter
                               else boring_file_filter
                    slurp_unboring (myfilt cur nboring) "."
            else co_slurp cur "."
    pend <- read_pending repository
    when (Verbose `elem` opts) $ putStrLn "diffing dir..."
    ftf <- filetype_function
    case smart_diff opts ftf cur work of
      Nothing -> case pend of
                 Just x | null (flatten x) -> return Nothing
                 _ -> return pend
      Just di->
        case pend of
        Nothing -> return $ Just di
        Just pp ->
            if AnyOrder `elem` opts
            then return $ liftM join_patches $
                 unempty $ flatten $ join_patches [pp,di]
            else return $ liftM (reorder_and_coalesce . join_patches) $
                 unempty $ flatten $ join_patches [pp,di]
    where myfilt s nboring f = slurp_has f s || nboring [f] /= []

read_repo :: Repository -> IO PatchSet
read_repo (Repo r _ (DarcsRepository _)) = DarcsRepo.read_repo r
read_repo (Repo r _ GitRepository) = GitRepo.read_repo r

sync_repo :: Repository -> IO ()
sync_repo (Repo r _ (DarcsRepository p)) =
    withCurrentDirectory r $ DarcsRepo.sync_repo p
-- this should probably update the Git cache
sync_repo (Repo _ _ GitRepository) = return ()

absolute_dir :: FilePath -> IO FilePath
absolute_dir = DarcsRepo.absolute_dir

prefsUrl :: Repository -> String
prefsUrl (Repo r _ (DarcsRepository _)) = r ++ "/_darcs/prefs"
prefsUrl (Repo r _ GitRepository) = r ++ "/.git/darcs-prefs"

unrevertUrl :: Repository -> String
unrevertUrl (Repo r _ (DarcsRepository _)) = r ++ "/_darcs/patches/unrevert"
unrevertUrl (Repo r _ GitRepository) = r ++ "/.git/darcs-unrevert"

applyToPristine :: Repository -> Patch -> IO ()
applyToPristine (Repo r _ (DarcsRepository p)) patch =
    withCurrentDirectory r $ applyPristine p patch
applyToPristine (Repo _ _ GitRepository) _ = return ()

-- writePatch returns an opaque token that should be passed to updateInventory.

data PatchToken = DarcsPatchToken !String | GitPatchToken !String

writePatch :: Repository -> [DarcsFlag] -> Patch -> IO (Patch, PatchToken)
writePatch (Repo dir _ (DarcsRepository _)) opts patch =
    withCurrentDirectory dir $
        do fp <- DarcsRepo.write_patch opts patch
           patch' <- gzReadPatchFileLazily fp
           return (patch', DarcsPatchToken fp)
writePatch (Repo dir _ GitRepository) _ patch =
    withCurrentDirectory dir $
        do cd <- getCurrentDirectory
           (patch', token) <- GitRepo.writePatch cd patch
           return (patch', GitPatchToken token)

-- this should be called with signals blocked
updateInventory :: Repository -> [(PatchInfo, PatchToken)] -> IO ()
updateInventory (Repo dir _ (DarcsRepository _)) l =
    withCurrentDirectory dir $
        DarcsRepo.add_to_inventory "." (map fst l)
updateInventory (Repo dir _ GitRepository) l =
    withCurrentDirectory dir $
        GitRepo.updateInventory (map (\(_,(GitPatchToken t)) -> t) l)

patchTokenToPatchFile :: PatchToken -> Maybe String
patchTokenToPatchFile (DarcsPatchToken fp) = Just fp
patchTokenToPatchFile _ = Nothing

patchSetToPatches :: PatchSet -> [Patch]
patchSetToPatches patchSet = catMaybes $ map snd $ reverse $ concat patchSet

getUMask :: [DarcsFlag] -> Maybe String
getUMask [] = Nothing
getUMask ((UMask u):_) = Just u
getUMask (_:l) = getUMask l

withRepoLock :: [DarcsFlag] -> (Repository -> IO a) -> IO a
withRepoLock opts job = do repository <- identifyRepository "."
                           let (Repo _ rf rt) = repository
                           case write_problem rf of
                               Nothing -> return ()
                               Just err -> fail err
                           let name = case rt of
                                        DarcsRepository _ -> "./_darcs/lock"
                                        GitRepository -> "./.git/lock"
                               wu = case (getUMask opts) of
                                      Nothing -> id
                                      Just u -> withUMask u
                           wu (withLock name (job repository))

#ifdef HAVEWX
takeRepoGuiLock :: IO Bool
takeRepoGuiLock =
 do (_,name) <- guiLockFileHelper
    takeLock name

releaseRepoGuiLock :: IO ()
releaseRepoGuiLock =
 do (_,name) <- guiLockFileHelper
    releaseLock name

withRepoGuiLockCanFail :: (Repository -> IO a) -> IO (Either () a)
withRepoGuiLockCanFail job =
 do (repository,name) <- guiLockFileHelper
    withLockCanFail name (job repository)

guiLockFileHelper :: IO (Repository, FilePath)
guiLockFileHelper =
 do repository <- identifyRepository "."
    let (Repo _ rf rt) = repository
    case write_problem rf of
      Nothing  -> return ()
      Just err -> fail err
    let name = case rt of
          (DarcsRepository _) -> "./_darcs/guilock"
          GitRepository       -> "./.git/guilock"
    return (repository, name)
#endif
\end{code}
