{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}


module System.Nemesis.Titan where

import System.Nemesis.Env
import System.Nemesis (Unit)

import Air.Env hiding (mod)
import Prelude ()
import Air.TH
import Air.Data.Record.SimpleLabel (get, set, mod, label)

import qualified Data.ByteString.Char8 as B
import qualified Data.UUID as UUID
import System.Directory
import System.Random
import System.FilePath
import System.Exit (ExitCode(..))
import qualified Control.Exception as E
import Control.Monad (forever)

import Test.Hspec
import Text.StringTemplate
import Text.Printf

import Data.Maybe (fromMaybe)

angel_template :: StringTemplate String
angel_template = newSTMP - [here|
server {
    exec = "runghc Nemesis $project_name$/run"
    stdout = "/dev/stdout"
    stderr = "/dev/stderr"
    delay = 0
}

code-reload {
    exec = "runghc Nemesis $project_name$/guard"
    stdout = "/dev/stdout"
    stderr = "/dev/stderr"
    delay = 0
}
|]

-- Live mode without auto recompile, e.g. `runghc Nemesis compile-and-kill` is run inside git-post-receive-hook
angel_live_template :: StringTemplate String
angel_live_template = newSTMP - [here|
server {
    exec = "runghc Nemesis $project_name$/run"
    stdout = "/dev/stdout"
    stderr = "/dev/stderr"
    delay = 0
}
|]

guard_template :: StringTemplate String
guard_template = newSTMP - [here|
guard :shell do
  event_time = Time.now
  update_time = Time.now
  update_interval = 0.1

  watch(%r{^src/.+hs\$}) do |m|
    puts "Changed #{m[0]}"
    event_time = Time.now
  end

  # compile at most once for every \$update_interval seconds
  Thread.new do
    while true
      sleep update_interval
      if event_time > update_time
        update_time = Time.now
        system("runghc Nemesis $project_name$/compile-and-kill")
      end
    end
  end

end
|]


haskell_template :: StringTemplate String
haskell_template = newSTMP - [here|
module Main where

import System.Nemesis.Titan
import Test.Hspec

spec :: IO ()
spec = hspec \$ do
  describe "$project_name$" \$ do
    it "should run spec" True

main = do
  with_spec spec halt

|]

titan_spec :: IO ()
titan_spec = hspec - do
  describe "Titan" - do
    it "should run spec" True

    it "should use templates" - do
      let text = render - setAttribute "project_name" "Main" angel_template

      -- puts text

      text `shouldSatisfy` (null > not)



data Config = Config
  {
    pid_name :: String
  , bin_directory :: String
  , config_directory :: String
  , haskell_source_directory :: String
  , project_name :: String
  , file_name :: String
  , ghc_arg_string :: String
  , ghc_default_arg_string :: String
  , guard_arg_string :: String
  , guard_default_arg_string :: String
  }
  deriving (Show, Eq)

mkLabel ''Config

instance Default Config where
  def = Config
    {
      pid_name = "uuid.txt"
    , bin_directory = ".bin"
    , config_directory = "config"
    , haskell_source_directory = "src"
    , project_name = "Main"
    , file_name = "Main.hs"
    , ghc_arg_string = def
    , ghc_default_arg_string = "-threaded"
    , guard_arg_string = def
    , guard_default_arg_string = "--no-bundler-warning --no-interactions"
    }


titan_with_config :: Config -> Unit
titan_with_config config = do
  namespace (config.project_name) - do

    let
      _project_name = config.project_name
      config_project_name_directory = config.config_directory / _project_name
      angel_path = config_project_name_directory / "Angel.conf"
      angel_live_path = config_project_name_directory / "AngelLive.conf"
      guard_path = config_project_name_directory / "Guardfile"

      pid_directory = config.bin_directory / _project_name
      pid_path = pid_directory / config.pid_name

      haskell_source_path = config.haskell_source_directory / config.file_name

    desc "Initialize a Titan node"
    task "init" - io - do
      createDirectoryIfMissing True config_project_name_directory
      createDirectoryIfMissing True (config.haskell_source_directory)

      let
        angel_file_content = render - setAttribute "project_name" _project_name angel_template
        angel_live_file_content = render - setAttribute "project_name" _project_name angel_live_template
        guard_file_content = render - setAttribute "project_name" _project_name guard_template
        haskell_file_content = render - setAttribute "project_name" _project_name haskell_template

      let {
        write_if_not_exist file_path str = do
          file_exist <- doesFileExist file_path
          if not - file_exist
            then
              B.writeFile file_path - B.pack str
            else do
              puts - file_path + " already exists!"
              return ()
      }

      write_if_not_exist angel_path angel_file_content
      write_if_not_exist angel_live_path angel_live_file_content
      write_if_not_exist guard_path guard_file_content
      write_if_not_exist haskell_source_path haskell_file_content


    let { get_and_create_if_missing_upid = do
      createDirectoryIfMissing True pid_directory

      pid_exist <- doesFileExist pid_path


      if pid_exist
        then do
          uuid <- B.readFile pid_path ^ B.unpack
          return uuid
        else do
          uuid <- randomIO ^ UUID.toString
          puts - "Created UPID: " + uuid
          B.writeFile pid_path - uuid.B.pack
          return - uuid
      }

    let { get_bin = do
      uuid <- get_and_create_if_missing_upid
      return - pid_directory / uuid
    }

    desc "Start the Titan managed process"
    task "titan:uuid compile" - do
      sh - "angel " + angel_path

    desc "Start the Titan managed process for deployment (no auto recompile)"
    task "titan-live:uuid compile" - do
      sh - "angel " + angel_live_path

    desc "Create a uuid for this process if not already exist"
    task "uuid" - do
      io - void - get_and_create_if_missing_upid

    desc "Compile the binary"
    task "compile" - do
      bin <- get_bin
      let { cmd =
        printf "ghc --make -i%s %s %s %s -o %s"
          (config.haskell_source_directory)
          (config.ghc_default_arg_string)
          (config.ghc_arg_string)
          haskell_source_path
          bin
        }

      -- puts cmd
      sh cmd

    desc "Start the process"
    task "run" - do
      bin <- get_bin
      sh - bin

    desc "Kill the process"
    task "kill" - do
      upid <- get_and_create_if_missing_upid
      sh - "killall -SIGTERM " + upid + "; true"

    desc "Compile and Kill"
    task "compile-and-kill: compile kill" - return ()

    desc "Start the Guard process"
    task "guard" - do
      sh - printf "guard %s %s -G %s"
        (config.guard_default_arg_string)
        (config.guard_arg_string)
        guard_path


  -- shortcut
  let shortcut_task_name = printf "t:%s/titan" (config.project_name)
      shortcut_description = printf "Short task name for %s/titan" (config.project_name)

  desc shortcut_description
  task shortcut_task_name - return ()


titan :: String -> Unit
titan _file_name = do
  let _project_name = _file_name.takeBaseName
  titan_with_config def {file_name = _file_name, project_name = _project_name}


data MacAppArgs = MacAppArgs
  {
    derived_data_path :: String
  , scheme_name :: Maybe String
  , target_name :: String
  , frameworks :: [String]
  , mac_app_config :: Config
  , mac_app_project_name :: Maybe String
  , mac_app_file_name :: Maybe String
  }
  deriving (Show)

mkLabel ''MacAppArgs

default_mac_app_config :: Config
default_mac_app_config =
  def.set __ghc_arg_string "-lobjc"

instance Default MacAppArgs where
  def = MacAppArgs
    {
      derived_data_path = "DerivedData"
    , scheme_name = def
    , target_name = "Hello World Application"
    , frameworks = ["Cocoa"]
    , mac_app_config = default_mac_app_config
    , mac_app_project_name = def
    , mac_app_file_name = def
    }

titan_mac_app :: MacAppArgs -> Unit
titan_mac_app args = do
  let
      _target_name = args.target_name
      _dashed_target_name = _target_name.map (\x -> if x.is ' ' then '-' else x)
      _project_name = args.mac_app_project_name.fromMaybe _dashed_target_name
      _file_name = args.mac_app_file_name.fromMaybe (args.mac_app_config.file_name)

      _new_ghc_arg_string = args.frameworks.map ("-framework" +) .join " "

      _config =
        args.mac_app_config
          .set __file_name _file_name
          .set __project_name _project_name
          .mod __ghc_arg_string (_new_ghc_arg_string + " " +)

  titan_with_config _config

  let
      _scheme_name = args.scheme_name.fromMaybe _target_name
      _derived_data_path = args.derived_data_path



  namespace _project_name - do

    let config = _config
        haskell_source_path = config.haskell_source_directory / config.file_name
        bin = config.bin_directory / _project_name / "dummy_binary"

    task ("clean") - do
      sh - printf "rm -rf %s" _derived_data_path
      sh - printf "mkdir %s" _derived_data_path
      sh - printf "rm %s" bin

    desc "Compile the binary"
    task ("compile:uuid") - do
      let { cmd =
        printf "ghc --make -i%s %s %s %s -o %s"
          (config.haskell_source_directory)
          (config.ghc_default_arg_string)
          (config.ghc_arg_string)
          haskell_source_path
          bin
        }

      sh cmd

      let xcode_build_cmd = printf "cd .. && xcodebuild -scheme '%s' > /dev/null" _scheme_name
      sh xcode_build_cmd

    task ("kill") - do
      let osascript = printf "tell application \"%s\" to quit" _target_name
      sh - printf "osascript -e '%s'" (osascript :: String)

    task ("run") - do
      sh - printf "cd %s; find . -name '%s' -exec '{}' \\;" _derived_data_path _target_name


  -- shortcut
  let shortcut_task_name = printf "t:%s/kill %s/titan" _project_name _project_name
      shortcut_description = printf "%s/kill then %s/titan" _project_name _project_name

  desc shortcut_description
  task shortcut_task_name - return ()

-- Helpers
safe_spec :: IO () -> IO ExitCode
safe_spec spec = E.handle (\e -> return (e :: ExitCode)) - do
  spec
  return ExitSuccess

halt :: IO ()
halt = forever - sleep (1 :: Double)

with_spec :: IO () -> IO b -> IO ()
with_spec spec process = do
  exit_code <- safe_spec spec

  case exit_code of
    ExitSuccess -> do
      fork - process
    _ ->
      return ()

  halt
