module Storage.Hashed.Monad
    ( virtualTreeIO, virtualTreeMonad
    , readFile, writeFile, createDirectory, rename, copy, unlink
    , fileExists, directoryExists, exists, withDirectory
    , currentDirectory
    , tree, TreeState, TreeMonad, TreeIO, runTreeMonad
    , initialState, replaceItem
    , findM, findFileM, findTreeM
    , TreeRO, TreeRW
    ) where
import Prelude hiding ( readFile, writeFile )
import Storage.Hashed.AnchoredPath
import Storage.Hashed.Tree
import Control.Applicative( (<$>) )
import Data.List( sortBy )
import Data.Int( Int64 )
import Data.Maybe( isNothing, isJust )
import qualified Data.ByteString.Lazy.Char8 as BL
import Control.Monad.RWS.Strict
import qualified Data.Map as M
type Changed = M.Map AnchoredPath (Int64, Int64) 
data TreeState m = TreeState { tree :: !(Tree m)
                             , changed :: !Changed
                             , changesize :: !Int64
                             , maxage :: !Int64
                             , updateHash :: TreeItem m -> m Hash
                             , update :: AnchoredPath -> TreeItem m -> TreeMonad m (TreeItem m) }
type TreeMonad m = RWST AnchoredPath () (TreeState m) m
type TreeIO = TreeMonad IO
class (Functor m, Monad m) => TreeRO m where
    currentDirectory :: m AnchoredPath
    withDirectory :: AnchoredPath -> m a -> m a
    expandTo :: AnchoredPath -> m AnchoredPath
    
    readFile :: AnchoredPath -> m BL.ByteString
    
    exists :: AnchoredPath -> m Bool
    
    directoryExists ::AnchoredPath -> m Bool
    
    fileExists :: AnchoredPath -> m Bool
class TreeRO m => TreeRW m where
    
    
    writeFile :: AnchoredPath -> BL.ByteString -> m ()
    createDirectory :: AnchoredPath -> m ()
    unlink :: AnchoredPath -> m ()
    rename :: AnchoredPath -> AnchoredPath -> m ()
    copy   :: AnchoredPath -> AnchoredPath -> m ()
initialState :: Tree m -> (TreeItem m -> m Hash)
                -> (AnchoredPath -> TreeItem m -> TreeMonad m (TreeItem m)) -> TreeState m
initialState t uh u = TreeState { tree = t
                                , changed = M.empty
                                , changesize = 0
                                , updateHash = uh
                                , maxage = 0
                                , update = u }
flush :: (Functor m, Monad m) => TreeMonad m ()
flush = do changed' <- map fst <$> M.toList <$> gets changed
           dirs' <- gets tree >>= \t -> return [ path | (path, SubTree _) <- list t ]
           modify $ \st -> st { changed = M.empty, changesize = 0 }
           forM_ (changed' ++ dirs' ++ [AnchoredPath []]) flushItem
runTreeMonad' :: (Functor m, Monad m) => TreeMonad m a -> TreeState m -> m (a, Tree m)
runTreeMonad' action initial = do
  (out, final, _) <- runRWST action (AnchoredPath []) initial
  return (out, tree final)
runTreeMonad :: (Functor m, Monad m) => TreeMonad m a -> TreeState m -> m (a, Tree m)
runTreeMonad action initial = do
  let action' = do x <- action
                   flush
                   return x
  runTreeMonad' action' initial
virtualTreeMonad :: (Functor m, Monad m) => TreeMonad m a -> Tree m -> m (a, Tree m)
virtualTreeMonad action t = runTreeMonad' action $
                               initialState t (\_ -> return NoHash) (\_ x -> return x)
virtualTreeIO :: TreeIO a -> Tree IO -> IO (a, Tree IO)
virtualTreeIO = virtualTreeMonad
modifyItem :: (Functor m, Monad m)
            => AnchoredPath -> Maybe (TreeItem m) -> TreeMonad m ()
modifyItem path item = do
  path' <- (`catPaths` path) `fmap` currentDirectory
  age <- gets maxage
  changed' <- gets changed
  let getsize (Just (File b)) = lift (BL.length `fmap` readBlob b)
      getsize _ = return 0
  size <- getsize item
  let change = case M.lookup path' changed' of
        Nothing -> size
        Just (oldsize, _) -> size  oldsize
  modify $ \st -> st { tree = modifyTree (tree st) path' item
                     , changed = M.insert path' (size, age) (changed st)
                     , maxage = age + 1
                     , changesize = (changesize st + change) }
renameChanged :: (Functor m, Monad m)
               => AnchoredPath -> AnchoredPath -> TreeMonad m ()
renameChanged from to = modify $ \st -> st { changed = rename' $ changed st }
  where rename' = M.fromList . map renameone . M.toList
        renameone (x, d) | from `isPrefix` x = (to `catPaths` relative from x, d)
                         | otherwise = (x, d)
        relative (AnchoredPath from') (AnchoredPath x) = AnchoredPath $ drop (length from') x
replaceItem :: (Functor m, Monad m)
            => AnchoredPath -> Maybe (TreeItem m) -> TreeMonad m ()
replaceItem path item = do
  path' <- (`catPaths` path) `fmap` currentDirectory
  modify $ \st -> st { tree = modifyTree (tree st) path' item }
flushItem :: forall m. (Monad m, Functor m) => AnchoredPath -> TreeMonad m ()
flushItem path =
  do current <- gets tree
     case find current path of
       Nothing -> return () 
       Just x -> do y <- fixHash x
                    new <- gets update >>= ($ y) . ($ path)
                    replaceItem path (Just new)
    where fixHash :: TreeItem m -> TreeMonad m (TreeItem m)
          fixHash f@(File (Blob con NoHash)) = do
            hash <- gets updateHash >>= \x -> lift $ x f
            return $ File $ Blob con hash
          fixHash (SubTree s) | treeHash s == NoHash =
            gets updateHash >>= \f -> SubTree <$> lift (addMissingHashes f s)
          fixHash x = return x
flushSome :: (Monad m, Functor m) => TreeMonad m ()
flushSome = do x <- gets changesize
               when (x > megs 100) $ do
                 remaining <- go =<< sortBy age <$> M.toList <$> gets changed
                 modify $ \s -> s { changed = M.fromList remaining }
  where go [] = return []
        go ((path, (size, _)):chs) = do
          x <- (\s -> s  size) <$> gets changesize
          flushItem path
          modify $ \s -> s { changesize = x }
          if (x > megs 50) then go chs
                           else return $ chs
        megs = (* (1024 * 1024))
        age (_, (_, a)) (_, (_, b)) = compare a b
instance (Functor m, Monad m) => TreeRO (TreeMonad m) where
    expandTo p =
        do t <- gets tree
           p' <- (`catPaths` p) `fmap` ask
           t' <- lift $ expandPath t p'
           modify $ \st -> st { tree = t' }
           return p'
    fileExists p =
        do p' <- expandTo p
           (isJust . (flip findFile p')) `fmap` gets tree
    directoryExists p =
        do p' <- expandTo p
           (isJust . (flip findTree p')) `fmap` gets tree
    exists p =
        do p' <- expandTo p
           (isJust . (flip find p')) `fmap` gets tree
    readFile p =
        do p' <- expandTo p
           t <- gets tree
           let f = findFile t p'
           case f of
             Nothing -> fail $ "No such file " ++ show p'
             Just x -> lift (readBlob x)
    currentDirectory = ask
    withDirectory dir act = do
      dir' <- expandTo dir
      local (\_ -> dir') act
instance (Functor m, Monad m) => TreeRW (TreeMonad m) where
    writeFile p con =
        do _ <- expandTo p
           modifyItem p (Just blob)
           flushSome
        where blob = File $ Blob (return con) hash
              hash = NoHash 
                            
                            
                            
                            
    createDirectory p =
        do _ <- expandTo p
           modifyItem p $ Just $ SubTree emptyTree
    unlink p =
        do _ <- expandTo p
           modifyItem p Nothing
    rename from to =
        do from' <- expandTo from
           to' <- expandTo to
           tr <- gets tree
           let item = find tr from'
               found_to = find tr to'
           unless (isNothing found_to) $
                  fail $ "Error renaming: destination " ++ show to ++ " exists."
           unless (isNothing item) $ do
                  modifyItem from Nothing
                  modifyItem to item
                  renameChanged from to
    copy from to =
        do from' <- expandTo from
           _ <- expandTo to
           tr <- gets tree
           let item = find tr from'
           unless (isNothing item) $ modifyItem to item
findM' :: forall m a. (Monad m, Functor m)
       => (Tree m -> AnchoredPath -> a) -> Tree m -> AnchoredPath -> m a
findM' what t path = fst <$> virtualTreeMonad (look path) t
  where look :: AnchoredPath -> TreeMonad m a
        look = expandTo >=> \p' -> flip what p' <$> gets tree
findM :: (Monad m, Functor m) => Tree m -> AnchoredPath -> m (Maybe (TreeItem m))
findM = findM' find
findTreeM :: (Monad m, Functor m) => Tree m -> AnchoredPath -> m (Maybe (Tree m))
findTreeM = findM' findTree
findFileM :: (Monad m, Functor m) => Tree m -> AnchoredPath -> m (Maybe (Blob m))
findFileM = findM' findFile