{-# LANGUAGE FlexibleInstances, GADTs, OverloadedStrings, RankNTypes,
             StandaloneDeriving #-}

{- |
   Module      : System.JBI.Commands
   Description : Running a specific build tool
   Copyright   : (c) Ivan Lazar Miljenovic
   License     : MIT
   Maintainer  : Ivan.Miljenovic@gmail.com



 -}
module System.JBI.Commands
  ( WrappedTool (..)
  , Valid
  , toolName
  , toolInformation
  , checkValidity
  , alreadyUsed
  , infoProjectDir
    -- * Commands
  , prepare
  , targets
  , build
  , repl
  , clean
  , test
  , bench
  , exec
  , run
  , update
  ) where

import System.JBI.Commands.BuildTool
import System.JBI.Commands.Tool
import System.JBI.Environment
import System.JBI.Tagged

import Control.Monad    (forM)
import Data.Aeson       (ToJSON(toJSON))
import Data.Function    (on)
import Data.Proxy       (Proxy(..))
import System.Directory (withCurrentDirectory)
import System.Exit      (ExitCode(ExitSuccess), die)

--------------------------------------------------------------------------------

data WrappedTool proxy where
  Wrapped :: (NamedTool bt) => proxy bt -> WrappedTool proxy

-- | Not made polymorphic as there might be extra data contained
--   within.
instance Eq (WrappedTool Proxy) where
  (==) = (==) `on` toolName

-- | Not really a valid instance as it doesn't produce Haskell code.
instance Show (WrappedTool Proxy) where
  show = toolName

deriving instance Show (WrappedTool ToolInformation)
deriving instance Show (WrappedTool Valid)

instance ToJSON (WrappedTool ToolInformation) where
  toJSON = withWrapped toJSON

withWrapped :: (forall bt. (NamedTool bt) => proxy bt -> res)
               -> WrappedTool proxy -> res
withWrapped f (Wrapped bt) = f bt

toolName :: WrappedTool proxy -> String
toolName = withWrapped prettyName

toolInformation :: GlobalEnv -> WrappedTool proxy -> IO (WrappedTool ToolInformation)
toolInformation env (Wrapped pr) = Wrapped <$> commandToolInformation env pr

--------------------------------------------------------------------------------

data Valid bt = Valid
  { command      :: !(Installed bt)
  , projectDir   :: !(Tagged bt ProjectRoot)
  , hasArtifacts :: !Bool
    -- ^ Only to be used with 'ensurePrepared', 'prepare', 'unprepared'.
  } deriving (Eq, Ord, Show, Read)

alreadyUsed :: WrappedTool Valid -> Bool
alreadyUsed = withWrapped hasArtifacts

infoProjectDir :: WrappedTool Valid -> ProjectRoot
infoProjectDir = withWrapped (stripTag . projectDir)

-- This is pretty ugly; one way to clean it up would be to use MaybeT.
checkValidity :: GlobalEnv -> WrappedTool proxy -> IO (Maybe (WrappedTool Valid))
checkValidity env (Wrapped p) = fmap Wrapped <$> check p
  where
    check :: (BuildTool bt) => proxy' bt -> IO (Maybe (Valid bt))
    check _ = do
      mInst <- commandInformation
      case mInst of
        Nothing   -> return Nothing
        Just inst -> do
          usbl <- canUseCommand env inst
          if not usbl
             then return Nothing
             else do mroot <- commandProjectRoot (path inst)
                     forM mroot $ \root ->
                       Valid inst root <$> hasBuildArtifacts root

runInProject :: (forall bt. (BuildTool bt) => Tagged bt CommandPath -> IO res)
                -> WrappedTool Valid -> IO res
runInProject f (Wrapped val) = withCurrentDirectory (stripTag (projectDir val))
                                                    (f (path (command val)))

prepareWrapped :: GlobalEnv -> WrappedTool Valid -> IO (WrappedTool Valid)
prepareWrapped env wt@(Wrapped val) = do
  ec <- runInProject (commandPrepare env) wt
  case ec of
    ExitSuccess -> do
      hasArt <- canUseCommand env (command val)
      if hasArt
         then return (Wrapped (val { hasArtifacts = True }))
         else die "Preparation failed"
    _           -> die "Could not prepare"

runPrepared :: (forall bt. (BuildTool bt) => GlobalEnv -> Tagged bt CommandPath -> IO res)
               -> GlobalEnv -> WrappedTool Valid -> IO res
runPrepared f env wv = do
  wv' <- if not (alreadyUsed wv)
            then prepareWrapped env wv
            else return wv
  runInProject (f env) wv'

--------------------------------------------------------------------------------
-- This mimics the actual command-level portion of BuildTool

prepare :: GlobalEnv -> WrappedTool Valid -> IO ExitCode
prepare env wv = prepareWrapped env wv >> return ExitSuccess
-- Explicitly prepare.

targets :: GlobalEnv -> WrappedTool Valid -> IO [ProjectTarget]
targets = runPrepared (const (fmap stripTags . commandTargets))

build :: Maybe ProjectTarget -> GlobalEnv -> WrappedTool Valid -> IO ExitCode
build targ = runPrepared (\env cp -> commandBuild env cp (tagInner (tag targ)))

repl :: Maybe ProjectTarget -> GlobalEnv -> WrappedTool Valid -> IO ExitCode
repl targ = runPrepared (\env cp -> commandRepl env cp (tagInner (tag targ)))

clean :: GlobalEnv -> WrappedTool Valid -> IO ExitCode
clean = runPrepared commandClean

test :: GlobalEnv -> WrappedTool Valid -> IO ExitCode
test = runPrepared commandTest

bench :: GlobalEnv -> WrappedTool Valid -> IO ExitCode
bench = runPrepared commandBench

exec :: String -> Args -> GlobalEnv -> WrappedTool Valid -> IO ExitCode
exec cmd args = runPrepared (\env cp -> commandExec env cp cmd args)

run :: ProjectTarget -> Args -> GlobalEnv -> WrappedTool Valid -> IO ExitCode
run targ args = runPrepared (\env cp -> commandRun env cp (tag targ) args)

update :: GlobalEnv -> WrappedTool Valid -> IO ExitCode
update = runPrepared commandUpdate