content: -- |
  -- Module      : Data.Git.Diff
  -- License     : BSD-style
  -- Maintainer  : Nicolas DI PRIMA <nicolas@di-prima.fr>
  -- Stability   : experimental
  -- Portability : unix
  --
  -- Basic Git diff methods.
  --
  
  module Data.Git.Diff
      (
      -- * Basic features
        BlobContent(..)
      , BlobState(..)
      , BlobStateDiff(..)
      , getDiffWith
      -- * Default helpers
      , GitDiff(..)
      , GitFileContent(..)
      , FilteredDiff(..)
      , GitFileRef(..)
      , GitFileMode(..)
      , TextLine(..)
      , defaultDiff
      , getDiff
      ) where
  
  import Data.List (find, filter)
  import Data.Char (ord)
  import Data.Git
  import Data.Git.Repository
  import Data.Git.Storage
  import Data.Git.Ref
  import Data.Git.Storage.Object
  import Data.ByteString.Lazy.Char8 as L
  
  import Data.Typeable
  import Data.Git.Diff.Patience (Item(..), diff)
  
  
  -- | represents a blob's content (i.e., the content of a file at a given
  -- reference).
  data BlobContent = FileContent [L.ByteString] -- ^ Text file's lines
                   | BinaryContent L.ByteString -- ^ Binary content
      deriving (Show)
  
  -- | This is a blob description at a given state (revision)
  data BlobState hash = BlobState
      { bsFilename :: EntPath
      , bsMode     :: ModePerm
      , bsRef      :: Ref hash
      , bsContent  :: BlobContent
      }
      deriving (Show)
  
  -- | Two 'BlobState' are equal if they have the same filename, i.e.,
  --
  -- > ((BlobState x _ _ _) == (BlobState y _ _ _)) = (x == y)
  instance Eq (BlobState hash) where
      (BlobState f1 _ _ _) == (BlobState f2 _ _ _) = f2 == f1
  
  -- | Represents a file state between two revisions
  -- A file (a blob) can be present in the first Tree's revision but not in the
  -- second one, then it has been deleted. If only in the second Tree's revision,
  -- then it has been created. If it is in the both, maybe it has been changed.
  data BlobStateDiff hash =
        OnlyOld   (BlobState hash)
      | OnlyNew   (BlobState hash)
      | OldAndNew (BlobState hash) (BlobState hash)
  
  buildListForDiff :: (Typeable hash, HashAlgorithm hash)
                   => Git hash -> Ref hash -> IO [BlobState hash]
  buildListForDiff git ref = do
      commit <- getCommit git ref
      tree   <- resolveTreeish git $ commitTreeish commit
      case tree of
          Just t -> do htree <- buildHTree git t
                       buildTreeList htree []
          _      -> error "cannot build a tree from this reference"
      where
          --buildTreeList :: HTree hash -> EntPath -> IO [BlobState hash]
          buildTreeList [] _ = return []
          buildTreeList ((d,n,TreeFile r):xs) pathPrefix = do
              content <- catBlobFile r
              let isABinary = isBinaryFile content
              listTail <- buildTreeList xs pathPrefix
              case isABinary of
                  False -> return $ (BlobState (entPathAppend pathPrefix n) d r (FileContent $ L.lines content)) : listTail
                  True  -> return $ (BlobState (entPathAppend pathPrefix n) d r (BinaryContent content)) : listTail
          buildTreeList ((_,n,TreeDir _ subTree):xs) pathPrefix = do
              l1 <- buildTreeList xs      pathPrefix
              l2 <- buildTreeList subTree (entPathAppend pathPrefix n)
              return $ l1 ++ l2
  
          --catBlobFile :: Ref hash -> IO L.ByteString
          catBlobFile blobRef = do
              mobj <- getObjectRaw git blobRef True
              case mobj of
                  Nothing  -> error "not a valid object"
                  Just obj -> return $ oiData obj
          getBinaryStat :: L.ByteString -> Double
          getBinaryStat bs = L.foldl' (\acc w -> acc + if isBin $ ord w then 1 else 0) 0 bs / (fromIntegral $ L.length bs)
              where
                  isBin :: Int -> Bool
                  isBin i
                      | i >= 0 && i <= 8   = True
                      | i == 12            = True
                      | i >= 14 && i <= 31 = True
                      | otherwise          = False
  
          isBinaryFile :: L.ByteString -> Bool
          isBinaryFile file = let bs = L.take 512 file
                              in  getBinaryStat bs > 0.0
  
  -- | generate a diff list between two revisions with a given diff helper.
  --
  -- Useful to extract any kind of information from two different revisions.
  -- For example you can get the number of deleted files:
  --
  -- > getdiffwith f 0 head^ head git
  -- >     where f (OnlyOld _) acc = acc+1
  -- >           f _           acc = acc
  --
  -- Or save the list of new files:
  --
  -- > getdiffwith f [] head^ head git
  -- >     where f (OnlyNew bs) acc = (bsFilename bs):acc
  -- >           f _            acc = acc
  getDiffWith :: (Typeable hash, HashAlgorithm hash)
              => (BlobStateDiff hash -> a -> a) -- ^ diff helper (State -> accumulator -> accumulator)
              -> a                              -- ^ accumulator
              -> Ref hash                       -- ^ commit reference (the original state)
              -> Ref hash                       -- ^ commit reference (the new state)
              -> Git hash                       -- ^ repository
              -> IO a
  getDiffWith f acc ref1 ref2 git = do
      commit1 <- buildListForDiff git ref1
      commit2 <- buildListForDiff git ref2
      return $ Prelude.foldr f acc $ doDiffWith commit1 commit2
      where
          doDiffWith :: [BlobState hash] -> [BlobState hash] -> [BlobStateDiff hash]
          doDiffWith []        []        = []
          doDiffWith [bs1]     []        = [OnlyOld bs1]
          doDiffWith []        (bs2:xs2) = (OnlyNew bs2):(doDiffWith [] xs2)
          doDiffWith (bs1:xs1) xs2       =
              let bs2Maybe = Data.List.find (\x -> x == bs1) xs2
              in  case bs2Maybe of
                      Just bs2 -> let subxs2 = Data.List.filter (\x -> x /= bs2) xs2
                                  in  (OldAndNew bs1 bs2):(doDiffWith xs1 subxs2)
                      Nothing  -> (OnlyOld bs1):(doDiffWith xs1 xs2)
  
  data TextLine = TextLine
      { lineNumber  :: Integer
      , lineContent :: L.ByteString
      }
  instance Eq TextLine where
    a == b = (lineContent a) == (lineContent b)
    a /= b = not (a == b)
  instance Ord TextLine where
    compare a b = compare (lineContent a) (lineContent b)
    a <  b     = (lineContent a) < (lineContent b)
    a <= b     = (lineContent a) <= (lineContent b)
    a >  b     = b < a
    a >= b     = b <= a
  
  data FilteredDiff = NormalLine (Item TextLine) | Separator
  
  data GitFileContent = NewBinaryFile
                      | OldBinaryFile
                      | NewTextFile  [TextLine]
                      | OldTextFile  [TextLine]
                      | ModifiedBinaryFile
                      | ModifiedFile [FilteredDiff]
                      | UnModifiedFile
  
  data GitFileMode = NewMode        ModePerm
                   | OldMode        ModePerm
                   | ModifiedMode   ModePerm ModePerm
                   | UnModifiedMode ModePerm
  
  data GitFileRef hash =
        NewRef        (Ref hash)
      | OldRef        (Ref hash)
      | ModifiedRef   (Ref hash) (Ref hash)
      | UnModifiedRef (Ref hash)
  
  -- | This is a proposed diff records for a given file.
  -- It contains useful information:
  --   * the filename (with its path in the root project)
  --   * a file diff (with the Data.Algorithm.Patience method)
  --   * the file's mode (i.e. the file priviledge)
  --   * the file's ref
  data GitDiff hash = GitDiff
      { hFileName    :: EntPath
      , hFileContent :: GitFileContent
      , hFileMode    :: GitFileMode
      , hFileRef     :: GitFileRef hash
      }
  
  -- | A default Diff getter which returns all diff information (Mode, Content
  -- and Binary) with a context of 5 lines.
  --
  -- > getDiff = getDiffWith (defaultDiff 5) []
  getDiff :: (Typeable hash, HashAlgorithm hash)
          => Ref hash
          -> Ref hash
          -> Git hash
          -> IO [GitDiff hash]
  getDiff = getDiffWith (defaultDiff 5) []
  
  -- | A default diff helper. It is an example about how you can write your own
  -- diff helper or you can use it if you want to get all of differences.
  defaultDiff :: Int                -- ^ Number of line for context
              -> BlobStateDiff hash
              -> [GitDiff hash]     -- ^ Accumulator
              -> [GitDiff hash]     -- ^ Accumulator with a new content
  defaultDiff _ (OnlyOld   old    ) acc =
      let oldMode    = OldMode (bsMode old)
          oldRef     = OldRef  (bsRef  old)
          oldContent = case bsContent old of
                           BinaryContent _ -> OldBinaryFile
                           FileContent   l -> OldTextFile (Prelude.zipWith TextLine [1..] l)
      in (GitDiff (bsFilename old) oldContent oldMode oldRef):acc
  defaultDiff _ (OnlyNew       new) acc =
      let newMode    = NewMode (bsMode new)
          newRef     = NewRef  (bsRef  new)
          newContent = case bsContent new of
                           BinaryContent _ -> NewBinaryFile
                           FileContent   l -> NewTextFile (Prelude.zipWith TextLine [1..] l)
      in (GitDiff (bsFilename new) newContent newMode newRef):acc
  defaultDiff context (OldAndNew old new) acc =
      let mode = if (bsMode old) /= (bsMode new) then ModifiedMode (bsMode old) (bsMode new)
                                                 else UnModifiedMode (bsMode new)
          ref = if (bsRef old) == (bsRef new) then UnModifiedRef (bsRef new)
                                              else ModifiedRef (bsRef old) (bsRef new)
      in case (mode, ref) of
             ((UnModifiedMode _), (UnModifiedRef _)) -> acc
             _ -> (GitDiff (bsFilename new) (content ref) mode ref):acc
      where content :: GitFileRef hash -> GitFileContent
            content (UnModifiedRef _) = UnModifiedFile
            content _                 = createDiff (bsContent old) (bsContent new)
  
            createDiff :: BlobContent -> BlobContent -> GitFileContent
            createDiff (FileContent a) (FileContent b) =
                let linesA = Prelude.zipWith TextLine [1..] a
                    linesB = Prelude.zipWith TextLine [1..] b
                in ModifiedFile $ diffGetContext context (diff linesA linesB)
            createDiff _ _ = ModifiedBinaryFile
  
  -- Used by diffGetContext
  data GitAccu = AccuBottom | AccuTop
  
  -- Context filter
  diffGetContext :: Int -> [Item TextLine] -> [FilteredDiff]
  diffGetContext 0 list = fmap NormalLine list
  diffGetContext context list =
      let (_, _, filteredDiff) = Prelude.foldr filterContext (0, AccuBottom, []) list
          theList = removeTrailingBoth filteredDiff
      in case Prelude.head theList of
          (NormalLine (Both l _)) -> if lineNumber l > 1 then Separator:theList else theList
          _ -> theList
      where -- only keep 'context'. The file is annalyzed from the bottom to the top.
            -- The accumulator here is a tuple3 with (the line counter, the
            -- direction and the list of elements)
            filterContext :: (Item TextLine) -> (Int, GitAccu, [FilteredDiff]) -> (Int, GitAccu, [FilteredDiff])
            filterContext b@(Both {}) (c, AccuBottom, acc) =
                if c < context then (c+1, AccuBottom, (NormalLine b):acc)
                               else (c  , AccuBottom, (NormalLine b)
                                                      :((Prelude.take (context-1) acc)
                                                      ++ [Separator]
                                                      ++ (Prelude.drop (context+1) acc)))
            filterContext b@(Both {}) (c, AccuTop, acc) =
                if c < context then (c+1, AccuTop   , (NormalLine b):acc)
                               else (0  , AccuBottom, (NormalLine b):acc)
            filterContext element (_, _, acc) =
                (0, AccuTop, (NormalLine element):acc)
  
            startWithSeparator :: [FilteredDiff] -> Bool
            startWithSeparator [] = False
            startWithSeparator (Separator:_) = True
            startWithSeparator ((NormalLine l):xs) =
                case l of
                    Both {} -> startWithSeparator xs
                    _       -> False
  
            removeTrailingBoth :: [FilteredDiff] -> [FilteredDiff]
            removeTrailingBoth diffList =
                let test = startWithSeparator diffList
                in  if test then Prelude.tail $ Prelude.dropWhile (\a -> not $ startWithSeparator [a]) diffList
                            else diffList
  