%  Copyright (C) 2007 David Roundy
%
%  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; if not, write to the Free Software Foundation,
%  Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.

\begin{code}
{-# OPTIONS -fglasgow-exts #-}
#include "gadts.h"
module Darcs.Repository.HashedIO ( HashedIO, applyHashed, readHashFile,
                                   copyHashed, syncHashed, copyPartialsHashed, listHashedContents,
                                   slurpHashed, hashSlurped ) where

import System.Posix.Types ( EpochTime )
import Control.Monad.State ( StateT, runStateT, modify, get, put, gets, lift )
import Control.Monad ( when )
import Data.Maybe ( isJust )
import System.IO.Unsafe ( unsafeInterleaveIO )

import Workaround ( createDirectoryIfMissing )
import Darcs.SlurpDirectory ( Slurpy(..), withSlurpy, undefined_time, undefined_size )
import Darcs.Repository.Prefs ( Cache, fetchFileUsingCache, writeFileUsingCache,
                                peekInCache, speculateFileUsingCache,
                                findFileMtimeUsingCache, setFileMtimeUsingCache )
import Darcs.Patch ( Patchy, apply )
import Darcs.IO ( ReadableDirectory(..), WriteableDirectory(..) )
import Darcs.Flags ( DarcsFlag(VerifyHash) )
import Darcs.Lock ( writeAtomicFilePS )
import Darcs.Utils ( withCurrentDirectory )
import Darcs.Progress ( debugMessage, beginTedious, endTedious, tediousSize, finishedOneIO, progress )
import FileName ( FileName, norm_path, fp2fn, fn2fp, fn2niceps, niceps2fn,
                  break_on_dir, own_name, super_name )
import FastPackedString ( PackedString, packString, unpackPS, linesPS, unlinesPS, nilPS, lengthPS )
import SHA1 ( sha1PS )

readHashFile :: Cache -> [DarcsFlag] -> String -> String -> IO (String,PackedString)
readHashFile c opts subdir hash =
    do debugMessage $ "Reading hash file "++hash++" from "++subdir++"/"
       (fn,hf) <- fetchFileUsingCache c subdir hash
       if VerifyHash `elem` opts && sha1PS hf /= hash
           then fail $ "Hash failed hash:  "++ fn
           else return (fn,hf)

applyHashed :: Patchy q => Cache -> [DarcsFlag] -> String -> q C(x y) -> IO String
applyHashed c fs h p = do s <- slurpHashed c fs h
                          let ms = withSlurpy s $ apply fs p
                          case ms of
                            Left e -> fail e
                            Right (s', ()) -> hashSlurped c fs s'
{-
applyHashed c fs h p = do (_,hd) <- runStateT (apply fs p) $
                                    HashDir { permissions = RW, cache = c,
                                              options = fs, rootHash = h }
                          return $ rootHash hd
-}

data HashDir r p = HashDir { permissions :: !r, cache :: !Cache,
                             options :: ![DarcsFlag], rootHash :: !String }
type HashedIO r p = StateT (HashDir r p) IO

data RO = RO
data RW = RW
{-
class Readable r where
    isRO :: r -> Bool
    isRO = const False
instance Readable RW
instance Readable RO where
    isRO RO = True
-}

instance ReadableDirectory (HashedIO r p) where
    mDoesDirectoryExist fn = do thing <- identifyThing fn
                                case thing of Just (D,_) -> return True
                                              _ -> return False
    mDoesFileExist fn = do thing <- identifyThing fn
                           case thing of Just (F,_) -> return True
                                         _ -> return False
    mIsFileExecutable _ = return False
    mInCurrentDirectory fn j | fn' == fp2fn "" = j
                             | otherwise =
                                 case break_on_dir fn' of
                                 Nothing -> do c <- readroot
                                               case geta D fn' c of
                                                 Nothing -> fail "dir doesn't exist mInCurrentDirectory..."
                                                 Just h -> inh h j
                                 Just (d,fn'') -> do c <- readroot
                                                     case geta D d c of
                                                       Nothing -> fail "dir doesn't exist..."
                                                       Just h -> inh h $ mInCurrentDirectory fn'' j
        where fn' = norm_path fn
    mGetDirectoryContents = map (\ (_,f,_) -> f) `fmap` readroot
    mReadFilePS fn = mInCurrentDirectory (super_name fn) $ do
                                          c <- readroot
                                          case geta F (own_name fn) c of
                                            Nothing -> fail $ " file don't exist... "++ fn2fp fn
                                            Just h -> readhash h

instance WriteableDirectory (HashedIO RW p) where
    mWithCurrentDirectory fn j
        | fn' == fp2fn "" = j
        | otherwise =
            case break_on_dir fn' of
            Nothing -> do c <- readroot
                          case geta D fn' c of
                            Nothing -> fail "dir doesn't exist in mWithCurrentDirectory..."
                            Just h -> do (h',x) <- withh h j
                                         writeroot $ seta D fn' h' c
                                         return x
            Just (d,fn'') -> do c <- readroot
                                case geta D d c of
                                  Nothing -> fail "dir doesn't exist..."
                                  Just h -> do (h',x) <- withh h $ mWithCurrentDirectory fn'' j
                                               writeroot $ seta D d h' c
                                               return x
        where fn' = norm_path fn
    mSetFileExecutable _ _ = return ()
    mWriteFilePS fn ps = do mexists <- identifyThing fn
                            case mexists of
                              Just (D,_) -> fail "can't write file over directory"
                              _ -> do h <- writeHashFile ps
                                      makeThing fn (F,h)
    mCreateDirectory fn = do h <- writeHashFile nilPS
                             exists <- isJust `fmap` identifyThing fn
                             when exists $ fail "can't mCreateDirectory over an existing object."
                             makeThing fn (D,h)
    mRename o n = do nexists <- isJust `fmap` identifyThing n
                     when nexists $ fail "mRename failed..."
                     mx <- identifyThing o
                     -- for backwards compatibility accept rename of nonexistent files.
                     case mx of Nothing -> return ()
                                Just x -> do rmThing o
                                             makeThing n x
    mRemoveDirectory = rmThing
    mRemoveFile = rmThing

identifyThing :: FileName -> HashedIO r p (Maybe (ObjType,String))
identifyThing fn | fn' == fp2fn "" = do h <- gets rootHash
                                        return $ Just (D, h)
                 | otherwise = case break_on_dir fn' of
                               Nothing -> getany fn' `fmap` readroot
                               Just (d,fn'') -> do c <- readroot
                                                   case geta D d c of
                                                     Nothing -> return Nothing
                                                     Just h -> inh h $ identifyThing fn''
        where fn' = norm_path fn

makeThing :: FileName -> (ObjType,String) -> HashedIO RW p ()
makeThing fn (o,h) = mWithCurrentDirectory (super_name $ norm_path fn) $
                     seta o (own_name $ norm_path fn) h `fmap` readroot >>= writeroot

rmThing :: FileName -> HashedIO RW p ()
rmThing fn = mWithCurrentDirectory (super_name $ norm_path fn) $
             do c <- readroot
                let c' = filter (\(_,x,_)->x/= own_name (norm_path fn)) c
                if length c' == length c - 1
                  then writeroot c'
                  else fail "obj doesn't exist in rmThing"

readhash :: String -> HashedIO r p PackedString
readhash h = do HashDir _ c opts _ <- get
                z <- lift $ unsafeInterleaveIO $ readHashFile c opts "pristine.hashed" h
                let (_,out) = z
                return out

readTediousHash :: String -> String -> HashedIO r p PackedString
readTediousHash k h = do lift $ finishedOneIO k h
                         readhash h

gethashmtime :: String -> HashedIO r p EpochTime
gethashmtime h = do HashDir _ c _ _ <- get
                    lift $ unsafeInterleaveIO $ findFileMtimeUsingCache c "pristine.hashed" h

withh :: String -> HashedIO RW p a -> HashedIO RW p (String,a)
withh h j = do hd <- get
               put $ hd { rootHash = h }
               x <- j
               h' <- gets rootHash
               put hd
               return (h',x)

inh :: String -> HashedIO r p a -> HashedIO r p a
inh h j = do hd <- get
             put $ hd { rootHash = h }
             x <- j
             put hd
             return x

safeInterleave :: HashedIO RO p a -> HashedIO r p a
safeInterleave job = do HashDir _ c opts h <- get
                        z <- lift $ unsafeInterleaveIO $ runStateT job
                             (HashDir { permissions = RO, cache = c, options = opts, rootHash = h })
                        let (x,_) = z
                        return x

readroot :: HashedIO r p [(ObjType, FileName, String)]
readroot = do haveitalready <- peekroot
              cc <- gets rootHash >>= readdir
              when (not haveitalready) $ speculate cc
              return cc
    where speculate :: [(a,b,String)] -> HashedIO r q ()
          speculate c = do cac <- gets cache
                           mapM_ (\(_,_,z) -> lift $ speculateFileUsingCache cac "pristine.hashed" z) c
          peekroot :: HashedIO r p Bool
          peekroot = do HashDir _ c _ h <- get
                        lift $ peekInCache c "pristine.hashed" h

writeroot :: [(ObjType, FileName, String)] -> HashedIO r p ()
writeroot c = do h <- writedir c
                 modify $ \hd -> hd { rootHash = h }

data ObjType = F | D deriving Eq

geta :: ObjType -> FileName -> [(ObjType, FileName, String)] -> Maybe String
geta o f c = do (o',h) <- getany f c
                if o == o' then Just h else Nothing

getany :: FileName -> [(ObjType, FileName, String)] -> Maybe (ObjType,String)
getany _ [] = Nothing
getany f ((o,f',h):_) | f == f' = Just (o,h)
getany f (_:r) = getany f r

seta :: ObjType -> FileName -> String -> [(ObjType, FileName, String)] -> [(ObjType, FileName, String)]
seta o f h [] = [(o,f,h)]
seta o f h ((_,f',_):r) | f == f' = (o,f,h):r
seta o f h (x:xs) = x : seta o f h xs

readdir :: String -> HashedIO r p [(ObjType, FileName, String)]
readdir hash = (parsed . linesPS) `fmap` readhash hash
    where parsed (t:n:h:rest) | t == dir = (D, niceps2fn n, unpackPS h) : parsed rest
                              | t == file = (F, niceps2fn n, unpackPS h) : parsed rest
          parsed _ = []
dir :: PackedString
dir = packString "directory:"
file :: PackedString
file = packString "file:"


writedir :: [(ObjType, FileName, String)] -> HashedIO r p String
writedir c = writeHashFile cps
    where cps = unlinesPS $ concatMap (\ (o,d,h) -> [showO o,fn2niceps d,packString h]) c++[nilPS]
          showO D = dir
          showO F = file

writeHashFile :: PackedString -> HashedIO r p String
writeHashFile ps = do c <- gets cache
                      opts <- gets options
                      lift $ writeFileUsingCache c opts "pristine.hashed" ps

slurpHashed :: Cache -> [DarcsFlag] -> String -> IO Slurpy
slurpHashed c opts h = fst `fmap` runStateT slh
                                  (HashDir { permissions = RO, cache = c,
                                             options = opts, rootHash = h })

slh :: HashedIO r p Slurpy
slh = do c <- readroot
         hroot <- gets rootHash
         lift $ beginTedious k
         safeInterleave $ SlurpDir rootdir (Just hroot) `fmap` mapM sl c
    where sl (F,n,h) = do ps <- safeInterleave $ readTediousHash k h
                          t <- gethashmtime h
                          let len = if length h == 75 then read (take 10 h)
                                                      else undefined_size
                          return $ SlurpFile n (Just h, t, len) ps
          sl (D,n,h) = inh h $ do c <- readroot
                                  lift $ tediousSize k (length c)
                                  lift $ finishedOneIO k h
                                  SlurpDir n (Just h) `fmap` mapM sl c
          k = "Reading pristine"

rootdir :: FileName
rootdir = fp2fn "."

hashSlurped :: Cache -> [DarcsFlag] -> Slurpy -> IO String
hashSlurped c opts sl = do beginTedious k
                           h <- fst `fmap` runStateT (hsl sl)
                                        (HashDir { permissions = RW, cache = c,
                                                   options = opts, rootHash = sha1PS nilPS })
                           endTedious k
                           return h
    where hsl (SlurpDir _ (Just h) _) = return h
          hsl (SlurpDir _ Nothing ss) = do lift $ tediousSize k (length ss)
                                           mapM hs ss >>= writedir
          hsl (SlurpFile _ (Just h,_,_) _) = return h
          hsl (SlurpFile _ _ x) = writeHashFile x
          hs (SlurpDir d (Just h) _) = progress k $ return (D, d, h)
          hs s@(SlurpDir d Nothing _) = do h <- hsl s
                                           lift $ finishedOneIO k h
                                           return (D, d, h)
          hs (SlurpFile f (Just h,_,_) _) = progress k $ return (F, f, h)
          hs s@(SlurpFile f _ _) = do h <- hsl s
                                      lift $ finishedOneIO k h
                                      return (F, f, h)
          k = "Writing pristine"

grab :: FileName -> Slurpy -> Maybe Slurpy
grab _ (SlurpFile _ _ _) = Nothing
grab fn (SlurpDir _ _ ss) = g ss where g [] = Nothing
                                       g (s@(SlurpDir fn' _ _):_) | fn' == fn = Just s
                                       g (s@(SlurpFile fn' _ _):_) | fn' == fn = Just s
                                       g (_:x) = g x

syncHashed :: Cache -> Slurpy -> String -> IO ()
syncHashed c s r = do runStateT sh $ HashDir {permissions=RW, cache=c, options=[], rootHash=r}
                      return ()
    where sh = do cc <- readroot
                  lift $ tediousSize k (length cc)
                  mapM_ sh' cc
          sh' (D,n,h) = case progress k $ grab n s of
                        Just s' -> lift $ syncHashed c s' h
                        Nothing -> return ()
          sh' (F,n,h) = case progress k $ grab n s of
                        Just (SlurpFile _ (_,t',l) x) ->
                            do t <- lift $ findFileMtimeUsingCache c "pristine.hashed" h
                               when (t' /= undefined_time && t' /= t) $
                                    do ps <- readhash h
                                       when (lengthPS ps == fromIntegral l && ps == x) $
                                            lift $ setFileMtimeUsingCache c "pristine.hashed" h t'
                        _ -> return ()
          k = "Synchronizing pristine"

copyHashed :: String -> Cache -> [DarcsFlag] -> String -> IO ()
copyHashed k c opts z = do runStateT cph $ HashDir { permissions = RO, cache = c,
                                                     options = opts, rootHash = z }
                           return ()
    where cph = do cc <- readroot
                   lift $ tediousSize k (length cc)
                   mapM_ cp cc
          cp (F,n,h) = do ps <- readhash h
                          lift $ finishedOneIO k (fn2fp n)
                          lift $ writeAtomicFilePS (fn2fp n) ps
          cp (D,n,h) = do lift $ createDirectoryIfMissing False (fn2fp n)
                          lift $ finishedOneIO k (fn2fp n)
                          lift $ withCurrentDirectory (fn2fp n) $ copyHashed k c opts h

copyPartialsHashed :: Cache -> [DarcsFlag] -> String -> [FilePath] -> IO ()
copyPartialsHashed c opts root = mapM_ (copyPartialHashed c opts root)

copyPartialHashed :: Cache -> [DarcsFlag] -> String -> FilePath -> IO ()
copyPartialHashed c opts root ff =
    do createDirectoryIfMissing True (basename ff)
       runStateT (cp $ fp2fn ff) $ HashDir { permissions = RO, cache = c,
                                             options=opts, rootHash = root }
       return ()
 where basename = reverse . dropWhile ('/' /=) . dropWhile ('/' ==) . reverse
       cp f = do mt <- identifyThing f
                 case mt of
                   Just (D,h) -> do lift $ createDirectoryIfMissing True (fn2fp f)
                                    lift $ withCurrentDirectory (fn2fp f) $ copyHashed "" c opts h
                   Just (F,h) -> do ps <- readhash h
                                    lift $ writeAtomicFilePS (fn2fp f) ps
                   Nothing -> return ()

listHashedContents :: String -> Cache -> [DarcsFlag] -> String -> IO [String]
listHashedContents k c opts root =
    do beginTedious k
       tediousSize k 1
       x <- fst `fmap` runStateT (lhc (D,fp2fn ".",root)) (HashDir RO c opts root)
       endTedious k
       return x
    where lhc :: (ObjType, FileName, String) -> HashedIO r a [String]
          lhc (D,dname,d) = do xs <- inh d $ readroot
                               lift $ finishedOneIO k (fn2fp dname)
                               lift $ tediousSize k (length $ filter (\(x,_,_) -> x == D) xs)
                               hcxs <- mapM lhc xs
                               return (d:concat hcxs)
          lhc (F,_,h) = return [h]

\end{code}
