{-# LANGUAGE ViewPatterns #-}

module Development.Bake.Git(
    SHA1, fromSHA1, toSHA1, ovenGit,
    gitPatchExtra, gitInit
    ) where

import Development.Bake.Core.Type
import General.Extra
import Development.Shake.Command
import Control.Monad.Extra
import Data.List.Extra
import General.HTML
import System.Directory.Extra
import System.FilePath
import Data.Maybe
import Data.Tuple.Extra
import Data.Char
import Data.Hashable
import Data.Monoid
import Prelude


data SHA1 = SHA1 Int String deriving (Show,Eq) -- a number of leading primes, followed by a valid SHA1

-- | Convert a SHA1 obtained from Git into a SHA1. Only done by ovenInit or ovenUpdate
toSHA1 :: String -> SHA1
toSHA1 x = checkSHA1 x $ SHA1 0 x

fromSHA1 :: SHA1 -> String
fromSHA1 (SHA1 _ x) = x

instance Stringy SHA1 where
    stringyFrom x = checkSHA1 b $ SHA1 (length a) b
        where (a,b) = span (== '\'') x
    stringyTo (SHA1 primes sha) = replicate primes '\'' ++ sha
    stringyPretty (SHA1 primes sha) = replicate primes '\'' ++ take 7 sha

-- either returns the second argument, or raises an error
checkSHA1 :: String -> a -> a
checkSHA1 x res
    | length x /= 40 = error $ "SHA1 for Git must be 40 characters long, got " ++ show x
    | not $ all (`elem` "0123456789abcdef") x = error $ "SHA1 for Git must be all lower case hex, got " ++ show x
    | otherwise = res


-- | Modify an 'Oven' to work with the Git version control system.
--   Requires the name of the repo (e.g. @https:\/\/github.com\/ndmitchell\/bake.git@)
--   and the name of a branch (e.g. @master@). You can optionally give a path fragment
--   which is used to clone into.
ovenGit :: String -> String -> Maybe FilePath -> Oven () () test -> Oven SHA1 SHA1 test
ovenGit repo branch (fromMaybe "." -> path) o = o
    {ovenInit = gitInit repo branch
    ,ovenUpdate = gitUpdate
    ,ovenPrepare = \s ps -> do gitCheckout s ps; ovenPrepare o () $ map (const ()) ps
    ,ovenPatchExtra = \s p -> gitPatchExtra s p =<< gitInitMirror
    ,ovenSupersede = \_ _ -> False
    }
    where
        gitSafe dir = do
            time_ $ cmd (Cwd dir) "git config user.email" ["https://github.com/ndmitchell/bake"]
            time_ $ cmd (Cwd dir) "git config user.name" ["Bake Continuous Integration"]

        -- initialise the mirror, or make it up to date
        gitInitMirror = traced "gitInitMirror" $ do
            -- make sure we descend one directory, since bake writes .bake.name
            mirror <- fmap (</> "mirror") $ createDir "../bake-git" [repo]
            createDirectoryIfMissing True mirror
            -- see http://blog.plataformatec.com.br/2013/05/how-to-properly-mirror-a-git-repository/
            ready <- doesFileExist $ mirror </> "HEAD"
            if ready then
                time_ $ cmd (Cwd mirror) "git fetch --prune"
             else do
                time_ $ cmd (Cwd mirror) "git clone --mirror" [repo] "."
                gitSafe mirror
            return mirror

        gitUpdate s ps = traced "gitUpdate" $ do
            gitCheckout s ps
            Stdout x <- time $ cmd (Cwd path) "git rev-parse" [branch]
            time_ $ cmd (Cwd path) "git push" [repo] [branch ++ ":" ++ branch]
            return $ toSHA1 $ trim x

        gitCheckout s ps = traced "gitCheckout" $ do
            createDirectoryIfMissing True path
            mirror <- gitInitMirror
            unlessM (doesDirectoryExist $ path </>".git") $ do
                time_ $ cmd (Cwd path) "git init"
                gitSafe path
                time_ $ cmd (Cwd path) "git remote add origin" [(if path == "." then "" else "../") ++ mirror]
            time_ $ cmd (Cwd path) "git fetch"
            time_ $ cmd (Cwd path) "git reset" -- to unwedge a previous merge conflict
            time_ $ cmd (Cwd path) "git checkout" [branch]
            time_ $ cmd (Cwd path) "git reset --hard" ["origin/" ++ branch]
            Stdout x <- time $ cmd (Cwd path) "git rev-parse HEAD"
            when (trim x /= fromSHA1 s) $ error $
                "The branch " ++ branch ++ " changed SHA1 independently of bake.\n" ++
                "Expected value: " ++ fromSHA1 s ++ "\n" ++
                "But has become: " ++ trim x
            forM_ ps $ \p ->
                time_ $ cmd (Cwd path) "git merge" (fromSHA1 p)


gitInit :: String -> String -> IO SHA1
gitInit repo branch = traced "gitInit" $ do
    Stdout hash <- time $ cmd "git ls-remote" [repo] [branch]
    case words $ concat $ takeEnd 1 $ lines hash of
        [] -> error "Couldn't find branch"
        x:xs -> return $ toSHA1 $ trim x


traced :: String -> IO a -> IO a
traced msg act = do
    putStrLn $ "% GIT: Begin " ++ msg
    res <- act
    putStrLn $ "% GIT: Finish " ++ msg
    return res


---------------------------------------------------------------------
-- DIFF UTILITIES

gitPatchExtra :: SHA1 -> Maybe SHA1 -> FilePath -> IO (String, String)
gitPatchExtra s Nothing dir = do
    Stdout full <- time $ cmd (Cwd dir) "git log --no-merges -n10 --pretty=format:%s" [fromSHA1 s]
    Stdout count <- time $ cmd (Cwd dir) "git rev-list --count" [fromSHA1 s]
    let summary = takeWhile (/= '\n') full
    return (renderHTML $ do str_ $ count ++ " patches"; br_; str_ summary
           ,renderHTML $ pre_ $ str_ full)

gitPatchExtra s (Just p) dir = do
    Stdout diff <- time $ cmd (Cwd dir)
        "git diff" [fromSHA1 s ++ "..." ++ fromSHA1 p]
    Stdout stat <- time $ cmd (Cwd dir)
        "git diff --stat" [fromSHA1 s ++ "..." ++ fromSHA1 p]
    Stdout log <- time $ cmd (Cwd dir)
        "git log --no-merges -n1 --pretty=format:%s" [fromSHA1 p]
    return (renderHTML $ do str_ $ reduceStat stat; br_; str_ $ take 120 $ takeWhile (/= '\n') log
           ,renderHTML $ pre_ $ do prettyStat stat; str_ "\n"; prettyDiff diff)


reduceStat :: String -> String
reduceStat = commasLimit 3 . map trim . map (takeWhile (/= '|')) . dropEnd 1 . lines


diff :: FilePath -> String
diff x = "diff:" ++ show (abs $ hash x)

-- |
-- > src/Paths.hs                          |   11 ++
-- > src/Test.hs                           |  258 ++++++++++++------------
-- > travis.hs                             |    4 +-
-- > 28 files changed, 1612 insertions(+), 1302 deletions(-)
prettyStat :: String -> HTML
prettyStat = unlines_ . maybe [] (uncurry snoc . (map f *** str_)) . unsnoc . map trimStart . lines
    where
        f x = a__ [href_ $ "#" ++ diff a] (str_ a) <> str_ b <> g c
            where (ab,c) = break (== '|') x
                  (a,b) = spanEnd isSpace ab
        g x@('+':_) = span__ [class_ "green"] (str_ a) <> g b
            where (a,b) = span (== '+') x
        g x@('-':_) = span__ [class_ "red"] (str_ a) <> g b
            where (a,b) = span (== '-') x
        g (x:xs) = str_ [x] <> g xs
        g [] = mempty


-- |
-- > diff --git a/bake.cabal b/bake.cabal
-- > index 1aa1251..785cecc 100755
-- > --- a/bake.cabal
-- > +++ b/bake.cabal
-- > @@ -1,7 +1,7 @@
-- >  cabal-version:      >= 1.10
-- >  build-type:         Simple
-- >  name:               bake
-- > -version:            0.1
-- > +version:            0.2
prettyDiff :: String -> HTML
prettyDiff = unlines_ . map f . lines
    where
        f x | "diff --git " `isPrefixOf` x =
            let files = [y | ab:'/':y <- drop 2 $ words x, ab `elem` "ab"] in
            a__ (take 1 [name_ $ diff y | y <- files]) mempty <>
            b_ (str_ x)
        f x | any (`isPrefixOf` x) ["index ","--- ","+++ "] = b_ $ str_ x
        f xs@('+':_) = span__ [class_ "green"] $ str_ xs
        f xs@('-':_) = span__ [class_ "red"] $ str_ xs
        f xs = str_ xs