-- | A crude, unsafe and preliminary solution to building B9 'SharedImage's
-- from Shake.
module B9.Shake.SharedImageRules
  ( customSharedImageAction
  , needSharedImage
  , enableSharedImageRules
  ) where

import B9
import qualified Data.Binary as Binary
import qualified Data.ByteString as ByteString
import qualified Data.ByteString.Builder as Builder
import Data.ByteString.Builder (stringUtf8)
import qualified Data.ByteString.Lazy as LazyByteString
import Development.Shake
import Development.Shake.Classes
import Development.Shake.Rule

-- | In order to use 'needSharedImage' and 'customSharedImageAction' you need to
-- call this action before using any of the aforementioned 'Rules'.
enableSharedImageRules :: B9ConfigOverride -> Rules ()
enableSharedImageRules b9inv = addBuiltinRule noLint sharedImageIdentity go
  where
    sharedImageIdentity :: BuiltinIdentity SharedImageName SharedImageBuildId
    sharedImageIdentity (SharedImageName k) (SharedImageBuildId v) =
      Just (LazyByteString.toStrict (Builder.toLazyByteString (stringUtf8 k <> stringUtf8 v)))
    go :: BuiltinRun SharedImageName SharedImageBuildId
    go nameQ mOldBIdBinary dependenciesChanged = do
      mCurrentBId <- getImgBuildId
      case mCurrentBId of
        Just currentBId ->
          let currentBIdBinary = encodeBuildId currentBId
           in if dependenciesChanged == RunDependenciesChanged && mOldBIdBinary == Just currentBIdBinary
                then return $ RunResult ChangedNothing currentBIdBinary currentBId
                else rebuild (Just currentBIdBinary)
        _ -> rebuild Nothing
      where
        getImgBuildId = liftIO (runB9ConfigActionWithOverrides (runLookupLocalSharedImage nameQ) b9inv)
        encodeBuildId :: SharedImageBuildId -> ByteString.ByteString
        encodeBuildId = LazyByteString.toStrict . Binary.encode
        rebuild :: Maybe ByteString.ByteString -> Action (RunResult SharedImageBuildId)
        rebuild mCurrentBIdBinary = do
          (_, act) <- getUserRuleOne nameQ (const Nothing) imgMatch
          act b9inv
          mNewBId <- getImgBuildId
          newBId <-
            maybe
              (error ("failed to get SharedImageBuildId for " ++ show nameQ ++ " in context of " ++ show b9inv))
              return
              mNewBId
          let newBIdBinary = encodeBuildId newBId
          let change =
                if Just newBIdBinary == mCurrentBIdBinary
                  then ChangedRecomputeSame
                  else ChangedRecomputeDiff
          return $ RunResult change newBIdBinary newBId
          where
            imgMatch (SharedImageCustomActionRule name mkImage) =
              if name == nameQ
                then Just mkImage
                else Nothing

-- | Add a dependency to the creation of a 'SharedImage'. The build action
-- for the shared image must have been supplied by e.g. 'customSharedImageAction'.
-- NOTE: You must call 'enableSharedImageRules' before this action works.
needSharedImage :: SharedImageName -> Action SharedImageBuildId
needSharedImage = apply1

-- | Specify an arbitrary action that is supposed to build the given shared
-- image identified by a 'SharedImageName'.
-- NOTE: You must call 'enableSharedImageRules' before this action works.
customSharedImageAction :: SharedImageName -> Action () -> Rules ()
customSharedImageAction b9img customAction = addUserRule (SharedImageCustomActionRule b9img customAction')
  where
    customAction' b9inv = do
      customAction
      mCurrentBuildId <- liftIO (runB9ConfigActionWithOverrides (runLookupLocalSharedImage b9img) b9inv)
      putLoud (printf "Finished custom action, for %s, build-id is: %s" (show b9img) (show mCurrentBuildId))
      maybe (errorSharedImageNotFound b9img) return mCurrentBuildId

type instance RuleResult SharedImageName = SharedImageBuildId

data SharedImageCustomActionRule =
  SharedImageCustomActionRule SharedImageName
                              (B9ConfigOverride -> Action SharedImageBuildId)
  deriving (Typeable)

errorSharedImageNotFound :: Monad m => SharedImageName -> m a
errorSharedImageNotFound = fail . printf "Error: %s not found." . show