{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE LambdaCase                 #-}
{-# LANGUAGE ScopedTypeVariables        #-}
{-# LANGUAGE TupleSections              #-}

{-|

Description: A monad for manipulating trees of files from git.

An 'EditTree' is a convenient representation of a 'Tree'.  The leaves of an 'EditTree' are either
the contents of a 'Blob', a sub-'EditTree', or a 'Sha1'.  This makes it easier to work with large
'Tree's, because subobjects are stored as hashes until you modify them.

-}

module Data.Git.EditTree where

import           Prelude hiding (fail)
import           Control.Monad.Fail
import           Control.Monad.State hiding (fail)
import qualified Data.ByteString       as B
import           Data.ByteString.Lazy  (ByteString)
import qualified Data.ByteString.Lazy  as BL
import           Data.Git
import           Data.Map              (Map)
import qualified Data.Map              as Map
import           System.Posix.FilePath


-- | A nice representation of a 'Tree'.
type EditTree = Map TreeEntry TreePart

-- | The leaves of an 'EditTree'---either a hash, 'Blob' data, or a subtree.
data TreePart = PartSha Sha1 | PartData ByteString | PartTree EditTree

-- | A monad for editing 'EditTree's.
newtype TreeEdit m a = TreeEdit { runTreeEdit :: StateT EditTree m a }
    deriving (Functor, Applicative, Monad, MonadState EditTree, MonadIO, MonadTrans, MonadFail)

-- | Turn the given treeish 'Sha1' into an 'EditTree' whose leaves are all hashes.
loadEditTree :: (MonadGit m, MonadFail m) => Sha1 -> m EditTree
loadEditTree r = do (Just (Tree ents)) <- findTreeish r
                    return $ fmap PartSha ents

-- | Traverse an 'EditTree', writing new objects, and return the 'Sha1' of the new 'Tree'.
writeEditTree :: MonadGit m => EditTree -> m Sha1
writeEditTree = writeTree . Tree <=< traverse partToSha
    where partToSha (PartSha  s) = return s
          partToSha (PartData b) = writeBlob $ Blob b
          partToSha (PartTree t) = writeEditTree t

-- | Run a 'TreeEdit' computation against an 'EditTree', *without* writing the new objects out.
don'tEditTree :: MonadGit m => EditTree -> TreeEdit m a -> m EditTree
don'tEditTree et te = execStateT (runTreeEdit te) et

-- | Run a 'TreeEdit' computation against an 'EditTree', writing new objects as they occur.
editTree :: MonadGit m => EditTree -> TreeEdit m a -> m EditTree
editTree et te = do et' <- don'tEditTree et te
                    _ <- writeEditTree et'
                    return et'

-- | Delete an entry from the 'EditTree'.
rm :: Monad m => TreeEntry -> TreeEdit m ()
rm = modify . Map.delete

-- | Run a 'TreeEdit' in the subtree at the given path.
cd :: (MonadFail m, MonadGit m) => RawFilePath -> TreeEdit m a -> TreeEdit m a
cd dir act = splitPathComponents dir >>= (`cd'` act)

-- | As 'cd', but with a list of path components.
cd' :: (MonadFail m, MonadGit m) => [PathComponent] -> TreeEdit m a -> TreeEdit m a
cd' [] te     = te
cd' (d:ds) te = cd1 d (cd' ds te)

-- | A one-level version of 'cd'.
cd1 :: (MonadFail m, MonadGit m) => PathComponent -> TreeEdit m a -> TreeEdit m a
cd1 d te = do old <- get
              let dir = Entry d TreeMode
              case old Map.! dir of
                PartTree et  -> put et
                PartSha  r   -> put =<< lift (loadEditTree r)
                _ -> error "cd1 exploded"
              ret <- te
              modify (\et -> Map.insert dir (PartTree et) old)
              return ret

-- | Place a new leaf with the given filename.
create :: Monad m => TreeEntry -> TreePart -> TreeEdit m ()
create name ent = modify (Map.insert name ent)

-- | Create a subtree with the given filename.
mkdir :: MonadFail m => PathComponent -> TreeEdit m ()
mkdir dir = modify (Map.insertWith (flip const) (Entry dir TreeMode) (PartTree mempty))

-- | Create a path into the tree and do some 'TreeEdit's in that location.
cdCreating :: (MonadFail m, MonadGit m) => RawFilePath -> TreeEdit m a -> TreeEdit m a
cdCreating path te = (`cdCreating'` te) =<< splitPathComponents path

-- | As 'cdCreating', but with a list of path components.
cdCreating' :: (MonadFail m, MonadGit m) => [PathComponent] -> TreeEdit m a -> TreeEdit m a
cdCreating' path te = go path
    where go [] = te
          go (d:ds) = mkdir d >> cd1 d (go ds)

-- | A shortcut to create a bunch of files at once.
createFiles :: Monad m => Map PathComponent B.ByteString -> TreeEdit m ()
createFiles m = sequence_ [create (Entry file BlobMode) $ PartData (BL.fromStrict blob)
                               | (file, blob) <- Map.toList m]