{-# LANGUAGE RecordWildCards, ScopedTypeVariables, TupleSections #-}

module Development.Shake.Internal.History.Shared(
    Shared, newShared,
    addShared, lookupShared,
    removeShared, listShared,
    sanityShared
    ) where

import Control.Exception
import Development.Shake.Internal.Value
import Development.Shake.Internal.History.Types
import Development.Shake.Internal.History.Symlink
import Development.Shake.Internal.Core.Database
import Development.Shake.Classes
import General.Binary
import General.Extra
import Data.List
import Control.Monad.Extra
import System.Directory.Extra
import System.FilePath
import System.IO.Extra
import Numeric
import Development.Shake.Internal.FileInfo
import General.Wait
import Development.Shake.Internal.FileName
import Data.Monoid
import Control.Monad.IO.Class
import Data.Maybe
import qualified Data.ByteString as BS
import Prelude


data Shared = Shared
    {globalVersion :: !Ver
    ,keyOp :: BinaryOp Key
    ,sharedRoot :: FilePath
    ,useSymlink :: Bool
    }

newShared :: Bool -> BinaryOp Key -> Ver -> FilePath -> IO Shared
newShared useSymlink keyOp globalVersion sharedRoot = pure Shared{..}


data Entry = Entry
    {entryKey :: Key
    ,entryGlobalVersion :: !Ver
    ,entryBuiltinVersion :: !Ver
    ,entryUserVersion :: !Ver
    ,entryDepends :: [[(Key, BS_Identity)]]
    ,entryResult :: BS_Store
    ,entryFiles :: [(FilePath, FileHash)]
    } deriving (Show, Eq)

putEntry :: BinaryOp Key -> Entry -> Builder
putEntry binop Entry{..} =
    putExStorable entryGlobalVersion <>
    putExStorable entryBuiltinVersion <>
    putExStorable entryUserVersion <>
    putExN (putOp binop entryKey) <>
    putExN (putExList $ map (putExList . map putDepend) entryDepends) <>
    putExN (putExList $ map putFile entryFiles) <>
    putEx entryResult
    where
        putDepend (a,b) = putExN (putOp binop a) <> putEx b
        putFile (a,b) = putExStorable b <> putEx a

getEntry :: BinaryOp Key -> BS.ByteString -> Entry
getEntry binop x
    | (x1, x2, x3, x) <- binarySplit3 x
    , (x4, x) <- getExN x
    , (x5, x) <- getExN x
    , (x6, x7) <- getExN x
    = Entry
        {entryGlobalVersion = x1
        ,entryBuiltinVersion = x2
        ,entryUserVersion = x3
        ,entryKey = getOp binop x4
        ,entryDepends = map (map getDepend . getExList) $ getExList x5
        ,entryFiles = map getFile $ getExList x6
        ,entryResult = getEx x7
        }
    where
        getDepend x | (a, b) <- getExN x = (getOp binop a, getEx b)
        getFile x | (b, a) <- binarySplit x = (getEx a, b)

hexed x = showHex (abs $ hash x) ""

-- | The path under which everything relating to a Key lives
sharedFileDir :: Shared -> Key -> FilePath
sharedFileDir shared key = sharedRoot shared </> ".shake.cache" </> hexed key

-- | The list of files containing Entry values, given a result of 'sharedFileDir'
sharedFileKeys :: FilePath -> IO [FilePath]
sharedFileKeys dir = do
    b <- doesDirectoryExist_ $ dir </> "_key"
    if not b then pure [] else listFiles $ dir </> "_key"

loadSharedEntry :: Shared -> Key -> Ver -> Ver -> IO [IO (Maybe Entry)]
loadSharedEntry shared@Shared{..} key builtinVersion userVersion =
    map f <$> sharedFileKeys (sharedFileDir shared key)
    where
        f file = do
            e@Entry{..} <- getEntry keyOp <$> BS.readFile file
            let valid = entryKey == key && entryGlobalVersion == globalVersion && entryBuiltinVersion == builtinVersion && entryUserVersion == userVersion
            pure $ if valid then Just e else Nothing


-- | Given a way to get the identity, see if you can find a stored cloud version
lookupShared :: Shared -> (Key -> Wait Locked (Maybe BS_Identity)) -> Key -> Ver -> Ver -> Wait Locked (Maybe (BS_Store, [[Key]], IO ()))
lookupShared shared ask key builtinVersion userVersion = do
    ents <- liftIO $ loadSharedEntry shared key builtinVersion userVersion
    flip firstJustWaitUnordered ents $ \act -> do
        me <- liftIO act
        case me of
            Nothing -> pure Nothing
            Just Entry{..} -> do
                -- use Nothing to indicate success, Just () to bail out early on mismatch
                let result x = if isJust x then Nothing else Just $ (entryResult, map (map fst) entryDepends, ) $ do
                        let dir = sharedFileDir shared entryKey
                        forM_ entryFiles $ \(file, hash) ->
                            copyFileLink (useSymlink shared) (dir </> show hash) file
                result <$> firstJustM id
                    [ firstJustWaitUnordered id
                        [ test <$> ask k | (k, i1) <- kis
                        , let test = maybe (Just ()) (\i2 -> if i1 == i2 then Nothing else Just ())]
                    | kis <- entryDepends]


saveSharedEntry :: Shared -> Entry -> IO ()
saveSharedEntry shared entry = do
    let dir = sharedFileDir shared (entryKey entry)
    createDirectoryRecursive dir
    forM_ (entryFiles entry) $ \(file, hash) ->
        unlessM (doesFileExist_ $ dir </> show hash) $
            copyFileLink (useSymlink shared) file (dir </> show hash)
    -- Write key after files to make sure cache is always useable
    let v = runBuilder $ putEntry (keyOp shared) entry
    let dirName = dir </> "_key"
    createDirectoryRecursive dirName
    -- #757, make sure we write this file atomically
    (tempFile, cleanUp) <- newTempFileWithin dir
    (BS.writeFile tempFile v >> renameFile tempFile (dirName </> hexed v)) `onException` cleanUp


addShared :: Shared -> Key -> Ver -> Ver -> [[(Key, BS_Identity)]] -> BS_Store -> [FilePath] -> IO ()
addShared shared entryKey entryBuiltinVersion entryUserVersion entryDepends entryResult files = do
    files <- mapM (\x -> (x,) <$> getFileHash (fileNameFromString x)) files
    saveSharedEntry shared Entry{entryFiles = files, entryGlobalVersion = globalVersion shared, ..}

removeShared :: Shared -> (Key -> Bool) -> IO ()
removeShared Shared{..} test = do
    dirs <- listDirectories $ sharedRoot </> ".shake.cache"
    deleted <- forM dirs $ \dir -> do
        files <- sharedFileKeys dir
        -- if any key matches, clean them all out
        b <- flip anyM files $ \file -> handleSynchronous (\e -> putStrLn ("Warning: " ++ show e) >> pure False) $
            evaluate . test . entryKey . getEntry keyOp =<< BS.readFile file
        when b $ removePathForcibly dir
        pure b
    liftIO $ putStrLn $ "Deleted " ++ show (length (filter id deleted)) ++ " entries"

listShared :: Shared -> IO ()
listShared Shared{..} = do
    dirs <- listDirectories $ sharedRoot </> ".shake.cache"
    forM_ dirs $ \dir -> do
        putStrLn $ "Directory: " ++ dir
        keys <- sharedFileKeys dir
        forM_ keys $ \key ->
            handleSynchronous (\e -> putStrLn $ "Warning: " ++ show e) $ do
                Entry{..} <- getEntry keyOp <$> BS.readFile key
                putStrLn $ "  Key: " ++ show entryKey
                forM_ entryFiles $ \(file,_) ->
                    putStrLn $ "    File: " ++ file

sanityShared :: Shared -> IO ()
sanityShared Shared{..} = do
    dirs <- listDirectories $ sharedRoot </> ".shake.cache"
    forM_ dirs $ \dir -> do
        putStrLn $ "Directory: " ++ dir
        keys <- sharedFileKeys dir
        forM_ keys $ \key ->
            handleSynchronous (\e -> putStrLn $ "Warning: " ++ show e) $ do
                Entry{..} <- getEntry keyOp <$> BS.readFile key
                putStrLn $ "  Key: " ++ show entryKey
                putStrLn $ "  Key file: " ++ key
                forM_ entryFiles $ \(file,hash) ->
                    checkFile file dir hash
    where
      checkFile filename dir keyHash = do
          let cachefile = dir </> show keyHash
          putStrLn $ "    File: " ++ filename
          putStrLn $ "    Cache file: " ++ cachefile
          ifM (not <$> doesFileExist_ cachefile)
              (putStrLn "      Error: cache file does not exist") $
              ifM ((/= keyHash) <$> getFileHash (fileNameFromString cachefile))
                  (putStrLn "      Error: cache file hash does not match stored hash")
                  (putStrLn "      OK")