    {-# 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
               , Diff(..)
               , diff
               , searchRevisions
               , smartRetrieve
               , richDirectory
               )

    where
    import Data.FileStore.Types

    import Control.Exception as E
    import Data.FileStore.Utils
    import Data.List (isInfixOf)
    import Data.Algorithm.Diff (Diff(..), getGroupedDiff)
    import System.FilePath ((</>))

    handleUnknownError :: E.SomeException -> IO a
    handleUnknownError = E.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 = E.catch (latest fs name >> E.throwIO ResourceExists)
                                                    (\e -> if e == NotFound
                                                     then save fs name author logMsg contents
                                                     else E.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) <- E.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 @[(Diff, [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 [Diff [String]]
    diff fs name Nothing id2 = do
      contents2 <- retrieve fs name id2
      return [Second (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) 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 <- E.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) -> E.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 E.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 = directory fs fp >>= mapM f
      where f r = E.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 directories")
            g res@(FSFile file) = do rev <- revision fs =<< latest fs ( fp </> file )
                                     return (res,Right rev)