{-# LANGUAGE LambdaCase        #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections     #-}
{-# LANGUAGE ViewPatterns      #-}

{-|

Description: References to git hashes---HEAD, branches, tags, etc.

A ref is a pointer to a hash (or "symref", which is a pointer to another ref).  Refs are just files
under @.git@ (usually @.git/refs@, though @HEAD@ is a notable exception) that contain the (40-byte)
hash to which they refer.  We factor refs into 'Ref's proper and 'RefFile's---the former denoting
a ref's name, and the latter the contents of ref file.

-}

module Data.Git.Ref
    (
      Ref(..)
    , RefFile(..)
    , mkRef
    , readRefFile
    , readPackedRefsFile
    ) where

import           Control.Applicative
import           Control.Monad.State
import           Data.Attoparsec.ByteString        as A
import           Data.Attoparsec.ByteString.Char8  (isSpace_w8)
import           Data.ByteString                   (ByteString)
import           Data.String
import           System.Posix.FilePath (RawFilePath, (</>))

import Data.Git.Internal.FileUtil
import Data.Git.Hash
import Data.Git.Internal.Parsers
import Data.Git.Paths
import Data.Git.RefName

-- | A reference to a git hash
data Ref = HEAD
         | Branch RefName              -- ^ branches under @refs/heads@
         | TagRef RefName (Maybe Sha1) -- ^ tags under @refs/tags@, possibly peeled
         | RemRef RemoteName RefName   -- ^ remote refs under @refs/remotes@
         | ExpRef RefName              -- ^ any path under @.git@
           deriving (Eq, Ord, Show)

instance IsString Ref where
    fromString s = maybe (error $ "can't parse ref: " ++ s) id $ mkRef (fromString s)

-- | The path of a 'Ref' relative to the git directory.
instance InRepo Ref where
    inRepo HEAD         = "HEAD"
    inRepo (Branch b)   = "refs/heads"   </> getRefName b
    inRepo (TagRef b _) = "refs/tags"    </> getRefName b
    inRepo (RemRef r b) = "refs/remotes" </> getRemoteName r </> getRefName b
    inRepo (ExpRef p)   = getRefName p

parseRef :: Parser Ref
parseRef = "HEAD" *> pure HEAD
           <|> Branch <$> ("refs/heads/"   *> parseRefName)
           <|> TagRef <$> ("refs/tags/"    *> parseRefName) <*> pure Nothing
           <|> RemRef <$> ("refs/remotes/" *> parseRemoteName <* "/") <*> parseRefName
           <|> ExpRef <$> parseRefName

-- | The contents of a file containing a 'Ref'.  Either a 'Sha1' or a "Symbolic Reference" (e.g.,
--   @ref: refs/heads/master@) to another 'Ref'.
data RefFile = ShaRef Sha1
             | SymRef Ref
               deriving (Eq, Ord, Show)

parseRefFile :: Parser RefFile
parseRefFile = SymRef <$> (string "ref: " *> parseRef <* eol)
           <|> ShaRef <$> (parseSha1Hex               <* eol)

parseRefName :: Parser RefName
parseRefName = do
  rn <- takeTill isSpace_w8
  maybe empty return $ refName rn

parseRemoteName :: Parser RemoteName
parseRemoteName = do
  rn <- A.takeWhile (/= 0o57)
  maybe empty return $ remoteName rn

-- | Try to parse a 'Ref'.
mkRef :: ByteString -> Maybe Ref
mkRef = either (const Nothing) Just . parseOnly parseRef

maybeReadFile :: RawFilePath -> IO (Maybe ByteString)
maybeReadFile fp = mwhenFileExists fp (liftIO . readRawFileS $ fp)

parseFile :: Parser a -> RawFilePath -> IO (Maybe a)
parseFile p fp = do file <- maybeReadFile fp
                    return $ case file of
                               Nothing -> Nothing
                               Just f  -> either (const Nothing) Just (parseOnly p f)

-- | Try to parse a 'RefFile' out of an actual file.
readRefFile :: RawFilePath -> IO (Maybe RefFile)
readRefFile = parseFile parseRefFile

parsePackedRef :: Parser (Ref, Sha1)
parsePackedRef = do
  sha <- parseSha1Hex
  void space
  ref <- parseRef
  eol
  case ref of
    TagRef t _ -> (,sha) . TagRef t <$> optional ("^" *> parseSha1Hex <* eol)
    _          -> return (ref, sha)

parsePackedRefs :: Parser [(Ref, Sha1)]
parsePackedRefs = endOfInput *> pure []
                  <|> (:) <$> parsePackedRef <*> parsePackedRefs
                  <|> skipLine *> parsePackedRefs

-- | Try to parse any given @packed-refs@ file.
readPackedRefsFile :: RawFilePath -> IO (Maybe [(Ref, Sha1)])
readPackedRefsFile = parseFile parsePackedRefs