{-# LANGUAGE ScopedTypeVariables #-}
{- |
   Module      : Data.FileStore.Generic
   Copyright   : Copyright (C) 2009 John MacFarlane, Gwern Branwen, Sebastiaan Visser
   License     : BSD 3

   Maintainer  : John MacFarlane <jgm@berkeley.edu>
   Stability   : alpha
   Portability : GHC 6.10 required

   Generic utility functions for working with filestores.
-}

module Data.FileStore.Generic
           ( modify
           , create
           , DI(..)
           , diff
           , searchRevisions
           , smartRetrieve
           , richDirectory
           )

where
import Data.FileStore.Types

import Control.Exception (throwIO, catch, SomeException, try)
import Data.FileStore.Utils
import Data.List (isInfixOf)
import Data.Algorithm.Diff (DI(..), getGroupedDiff)
import System.FilePath ((</>))
import Prelude hiding (catch)

handleUnknownError :: SomeException -> IO a
handleUnknownError = throwIO . UnknownError . show

-- | Like save, but first verify that the resource name is new.  If not, throws a 'ResourceExists'
-- error.
create :: Contents a
       => FileStore
       -> FilePath          -- ^ Resource to create.
       -> Author            -- ^ Author of change.
       -> Description       -- ^ Description of change.
       -> a                 -- ^ Contents of resource.
       -> IO ()
create fs name author logMsg contents = catch (latest fs name >> throwIO ResourceExists)
                                                (\e -> if e == NotFound
                                                 then save fs name author logMsg contents
                                                 else throwIO e)

-- | Modify a named resource in the filestore.  Like save, except that a revision ID
-- must be specified.  If the resource has been modified since the specified revision,
-- @Left@ merge information is returned.  Otherwise, @Right@ the new contents are saved.  
modify  :: Contents a
        => FileStore
        -> FilePath          -- ^ Resource to create.
        -> RevisionId        -- ^ ID of previous revision that is being modified.
        -> Author            -- ^ Author of change.
        -> Description       -- ^ Description of change.
        -> a                 -- ^ Contents of resource.
        -> IO (Either MergeInfo ())
modify fs name originalRevId author msg contents = do
  latestRevId <- latest fs name
  latestRev <- revision fs latestRevId
  if idsMatch fs originalRevId latestRevId
     then save fs name author msg contents >> return (Right ())
     else do
       latestContents <- retrieve fs name (Just latestRevId)
       originalContents <- retrieve fs name (Just originalRevId)
       (conflicts, mergedText) <- catch 
                                  (mergeContents ("edited", toByteString contents) (originalRevId, originalContents) (latestRevId, latestContents))
                                  handleUnknownError
       return $ Left (MergeInfo latestRev conflicts mergedText)

-- | Return a unified diff of two revisions of a named resource.
-- Format of the diff is a list @[(DI, [String])]@, where
-- @DI@ is @F@ (in first document only), @S@ (in second only),
-- or @B@ (in both), and the list is a list of lines (without
-- newlines at the end).
diff :: FileStore
     -> FilePath      -- ^ Resource name to get diff for.
     -> Maybe RevisionId  -- ^ @Just@ old revision ID, or @Nothing@ for empty.
     -> Maybe RevisionId  -- ^ @Just@ oew revision ID, or @Nothing@ for latest.
     -> IO [(DI, [String])]
diff fs name Nothing id2 = do
  contents2 <- retrieve fs name id2
  return [(S, lines contents2)]   -- no need to run getGroupedDiff here - diff vs empty document 
diff fs name id1 id2 = do
  contents1 <- retrieve fs name id1
  contents2 <- retrieve fs name id2
  return $ getGroupedDiff (lines contents1) (lines contents2)

-- | Return a list of all revisions that are saved with the given
-- description or with a part of this description.
searchRevisions :: FileStore
                -> Bool              -- ^ When true the description must
                                     --   match exactly, when false partial
                                     --   hits are allowed.
                -> FilePath          -- ^ The resource to search history for.
                -> Description       -- ^ Revision description to search for.
                -> IO [Revision]

searchRevisions repo exact name desc = do
  let matcher = if exact
                then (== desc)
                else (desc `isInfixOf`)
  revs <- (history repo) [name] (TimeRange Nothing Nothing)
  return $ Prelude.filter (matcher . revDescription) revs

-- | Try to retrieve a resource from the repository by name and possibly a
-- revision identifier. When retrieving a resource by revision identifier fails
-- this function will try to fetch the latest revision for which the
-- description matches the given string.
smartRetrieve
  :: Contents a
  => FileStore
  -> Bool            -- ^ @True@ for exact description match, @False@ for partial match.
  -> FilePath        -- ^ Resource name to retrieve.
  -> Maybe String    -- ^ @Just@ revision ID or description, or @Nothing@ for empty.
  -> IO a
smartRetrieve fs exact name mrev = do
  edoc <- try (retrieve fs name mrev)
  case (edoc, mrev) of
    
    -- Regular retrieval using revision identifier succeeded, use this doc.
    (Right doc, _) -> return doc

    -- Retrieval of latest revision failed, nothing we can do about this.
    (Left e, Nothing) -> throwIO (e :: FileStoreError)

    -- Retrieval failed, we can try fetching a revision by the description.
    (Left _, Just rev) -> do
      revs <- searchRevisions fs exact name rev
      if Prelude.null revs

        -- No revisions containing this description.
        then throwIO NotFound

        -- Retrieve resource for latest matching revision.
        else retrieve fs name (Just $ revId $ Prelude.head revs)

-- | Like 'directory', but returns information about the latest revision.
richDirectory :: FileStore -> FilePath -> IO [(Resource, Either String Revision)]
richDirectory fs fp = do
  rs <- directory fs fp
  mapM f rs
  where f r = Control.Exception.catch (g r) (\(e :: FileStoreError)-> return $ ( r, Left . show $ e ) )
        g r@(FSDirectory _dir) = return (r,Left "richDirectory, we don't care about revision info for repos")
        g res@(FSFile file) = do rev <- revision fs =<< latest fs ( fp </> file )
                                 return (res,Right rev)