{-|

Module      : Azubi.Core.StateExecutors.LocalUnixStateExecutor
Description : 'StateExecutor' for Unix machines
Copyright   : (c) Ingolf Wagner, 2017
License     : GPL-3
Maintainer  : azubi@ingolf-wagner.de
Stability   : experimental
Portability : POSIX

Run 'State's on a Unix machine.

-}

module Azubi.Core.StateExecutors.LocalUnixStateExecutor ( UnixSystem(..)
                                                        , Verbosity(..) ) where

import Azubi.Core.Model
import Azubi.Core.StateExecutor

import System.Directory

import System.Process
import System.Exit

import System.Posix.Files (createSymbolicLink, readSymbolicLink)

import Data.Algorithm.Diff
import Data.Algorithm.DiffOutput



data Verbosity = Verbose | Silent

{-|

Unix System like Linux, AIX or OSX

<https://en.wikipedia.org/wiki/Unix>

-}
data UnixSystem = UnixSystem { verbose :: Verbosity }

instance LocalStateExecute UnixSystem where
  setup _ = return ()
  tearDown _ _ = return ()


  executeState systemConfig (State checks commands comment) = do
    runComment' comment
    checkResult <- collectCheckResults systemConfig checks
    case checkResult of
      Yes -> return Fulfilled
      No -> do
        commandResult <- collectRunResults systemConfig commands
        case commandResult of
          Success -> return Fulfilled
          Failure -> return Unfulfilled

  executeState systemConfig (States check states comment) = do
    runComment' comment
    result <- collectCheckResults systemConfig check
    case result of
      Yes -> return Fulfilled
      No -> collectStateResults states
    where
      collectStateResults :: [State] -> IO StateResult
      collectStateResults [] = return Fulfilled
      collectStateResults (x:xs) = do
        result <- executeState systemConfig x
        case result of
          Unfulfilled -> return Unfulfilled
          Fulfilled -> collectStateResults xs

-- | unroll a number of Check(s)
-- | If one fail, they all fail
collectCheckResults :: UnixSystem -> [Check] -> IO CheckResult
collectCheckResults _ [] = return Yes
collectCheckResults systemConfig (check:rest) = do
        result <- runCheck' systemConfig check
        case result of
          Yes -> collectCheckResults systemConfig rest
          No -> return No

-- | unroll a number of Run Commands
-- | if one fail, they all fail
collectRunResults :: UnixSystem -> [Command] -> IO CommandResult
collectRunResults _ [] = return Success
collectRunResults systemConfig (command:rest) = do
  result <- runCommand' systemConfig command
  case result of
    Success -> collectRunResults systemConfig rest
    Failure -> return Failure

-- | Run a command
runCommand' :: UnixSystem -> Command -> IO CommandResult
runCommand' _ (CreateFolder path) = do
  runComment' $ Just $ "create directory " ++ path
  createDirectoryIfMissing True path
  return Success

runCommand' _ (FileContent path content) = do
  runComment' $ Just $ "write content to " ++ path
  writeFile path $ unlines content
  return Success

runCommand' _ (CreateSymlink path target) = do
  runComment' $ Just $ "create link " ++ path ++ " to " ++ target
  createSymbolicLink target path
  return Success

runCommand' systemConfig (Run command arguments comment) = do
  runComment' comment
  result <- runProcess' systemConfig [command] arguments
  case result of
    ExitSuccess -> return Success
    _ -> return Failure

runCommand' _ (Remove path) = do
  runComment' (Just $ "remove " ++ path)
  removePathForcibly path
  return Success


-- | show a comment
runComment' :: Maybe Comment -> IO ()
runComment' (Just comment) = echo' [comment]
runComment' Nothing = return ()



-- | Run a Check
runCheck' :: UnixSystem -> Check -> IO CheckResult
runCheck' _ (FolderExists path) = do
  behind <- whatIsBehind' path
  case behind of
    IsFolder -> return Yes
    _ -> return No

runCheck' _ (SymlinkExists path target) = do
  behind <- whatIsBehind' path
  case behind of
    IsSymlink -> do
      behindTarget <- readSymbolicLink path
      if behindTarget == target
      then return Yes
      else return No
    _ -> return No

runCheck' _ (HasFileContent path content) = do
  behind <- whatIsBehind' path
  case behind of
    IsFile -> checkContent
    _ -> return No
  where
    checkContent = do
      file <- readFile path
      currentContent <- return $ lines file
      diff <- return $ getGroupedDiff currentContent content
      case diff of
        (Both _ _):[] -> return Yes
        _ -> do
            echo' [ppDiff diff]
            return No
runCheck' systemConfig (Check command args comment) = do
  runComment' comment
  result <- runProcess' systemConfig [command] args
  case result of
    ExitSuccess -> return Yes
    _ -> return No

runCheck' systemConfig  (Not check ) = do
  result <- runCheck' systemConfig check
  case result of
    No -> return Yes
    Yes  -> return No

runCheck' _ AlwaysYes = return Yes

runCheck' _ (DoesExist path) = do
  behind <- whatIsBehind' path
  case behind of
    DoesNotExist -> return No
    _ -> return Yes


data FileType = IsFile
              | DoesNotExist
              | IsSymlink
              | IsFolder
              deriving (Show, Eq, Enum)

-- | helper function to check whats behind a path
whatIsBehind' :: String -> IO FileType
whatIsBehind' path = do
  checkFile <- doesFileExist path
  checkFolder <- doesDirectoryExist path
  case (checkFolder, checkFile) of
    (True,  _) -> return IsFolder
    (False, True) -> do
      checkSymlink <- pathIsSymbolicLink path
      if checkSymlink
      then return IsSymlink
      else return IsFile
    (False, False) -> return DoesNotExist


-- | simple print function
echo' :: [String] -> IO ()
echo' text = putStrLn $ unwords $ "[Azubi]":text

-- | run a process and wait until it's finished
-- | return the exit code
runProcess' :: UnixSystem -> [String] -> [String] -> IO ExitCode
runProcess' systemConfig command args =  do
  (_, _ , _ , checkHandle ) <- createProcess (shell $ unwords $ command ++ args ){ std_out = stdOutHandle }
  waitForProcess checkHandle
  where
    stdOutHandle :: StdStream
    stdOutHandle =
      case (verbose systemConfig) of
        Verbose -> Inherit
        Silent -> NoStream