{-# LANGUAGE OverloadedStrings #-}

module MOO.Builtins.Tasks ( builtins ) where

import Control.Applicative ((<$>))
import Control.Arrow ((&&&))
import Control.Concurrent (forkIO, killThread)
import Control.Concurrent.STM (atomically, newEmptyTMVar, takeTMVar, putTMVar)
import Control.Monad (void)
import Control.Monad.Cont (callCC)
import Control.Monad.Reader (asks)
import Control.Monad.State (gets, modify, get)
import Data.List (sort)
import Data.Time (getCurrentTime, addUTCTime, diffUTCTime)
import Data.Time.Clock.POSIX (utcTimeToPOSIXSeconds)

import qualified Data.HashMap.Lazy as HM
import qualified Data.Set as S

import MOO.Builtins.Common
import {-# SOURCE #-} MOO.Builtins
import MOO.Object
import MOO.Parser
import MOO.Task
import MOO.Types
import MOO.Verb

import qualified MOO.String as Str

{-# ANN module ("HLint: ignore Use camelCase" :: String) #-}

-- | ยง 4.4.6 MOO-Code Evaluation and Task Manipulation
builtins :: [Builtin]
builtins = [
    bf_raise
  , bf_call_function
  , bf_function_info
  , bf_eval
  , bf_set_task_perms
  , bf_caller_perms
  , bf_ticks_left
  , bf_seconds_left
  , bf_task_id
  , bf_suspend
  , bf_resume
  , bf_queue_info
  , bf_queued_tasks
  , bf_kill_task
  , bf_callers
  , bf_task_stack
  ]

bf_raise = Builtin "raise" 1 (Just 3)
           [TAny, TStr, TAny] TAny $ \(code : optional) ->
  let [Str message, value] =
        defaults optional [Str $ Str.fromText $ toText code, zero]
  in raiseException code message value

bf_call_function = Builtin "call_function" 1 Nothing
                   [TStr] TAny $ \(Str func_name : args) ->
  callBuiltin (toId func_name) args

bf_function_info = Builtin "function_info" 0 (Just 1)
                   [TStr] TLst $ \args -> case args of
  []         -> return $ fromListBy formatInfo $ HM.elems builtinFunctions
  [Str name] -> maybe (raise E_INVARG) (return . formatInfo) $
                HM.lookup (toId name) builtinFunctions

  where formatInfo :: Builtin -> Value
        formatInfo Builtin { builtinName     = name
                           , builtinMinArgs  = min
                           , builtinMaxArgs  = max
                           , builtinArgTypes = types
                           } =
          fromList [ Str $ fromId name
                   , Int $ fromIntegral min
                   , Int $ maybe (-1) fromIntegral max
                   , fromListBy (Int . typeCode) types
                   ]

bf_eval = Builtin "eval" 1 (Just 1) [TStr] TLst $ \[Str string] ->
  checkProgrammer >> case parseProgram (Str.toText string) of
    Right program -> do
      (programmer, this, player) <- frame $ \frame ->
        (permissions frame, initialThis frame, initialPlayer frame)
      let verb = initVerb { verbNames   = "Input to EVAL"
                          , verbProgram = program
                          , verbOwner   = programmer
                          , verbPermD   = True
                          }
          vars = mkVariables [ ("player", Obj player)
                             , ("caller", Obj this)
                             ]
      value <- evalFromFunc "eval" 0 $
               runVerb verb initFrame { variables     = vars
                                      , initialPlayer = player
                                      }
      return $ fromList [truthValue True, value]
    Left errors ->
      return $ fromList [truthValue False,
                         fromListBy (Str . Str.fromString) errors]

bf_set_task_perms = Builtin "set_task_perms" 1 (Just 1)
                    [TObj] TAny $ \[Obj who] -> do
  checkPermission who
  modifyFrame $ \frame -> frame { permissions = who }
  return zero

bf_caller_perms = Builtin "caller_perms" 0 (Just 0) [] TObj $ \[] ->
  Obj . objectForMaybe <$> caller permissions

bf_ticks_left = Builtin "ticks_left" 0 (Just 0) [] TInt $ \[] ->
  Int . fromIntegral <$> gets ticksLeft

bf_seconds_left = Builtin "seconds_left" 0 (Just 0) [] TInt $ \[] -> do
  (limit, start) <- gets (secondsLimit &&& startTime)
  -- We calculate the elapsed time within the following IO action rather than
  -- simply return the current time so that the action depends on some local
  -- state and thus will be re-executed each time and not be optimized away to
  -- a constant. Unsafe indeed!
  elapsed <- unsafeIOtoMOO $ (`diffUTCTime` start) <$> getCurrentTime
  return $ Int $ fromIntegral $ limit - round elapsed

bf_task_id = Builtin "task_id" 0 (Just 0) [] TInt $ \[] ->
  Int . fromIntegral <$> asks (taskId . task)

bf_suspend = Builtin "suspend" 0 (Just 1) [TNum] TAny $ \optional -> do
  let (maybeSeconds : _) = maybeDefaults optional

  {- maybeMicrosecs <- mapM getDelay maybeSeconds  -- requires GHC 7.10 -}
  maybeMicrosecs <- maybe (return Nothing) (fmap Just . getDelay) maybeSeconds

  state <- get

  estimatedWakeup <- case maybeMicrosecs of
    Just usecs
      | time < now || time > endOfTime -> raise E_INVARG
      | otherwise                      -> return time
      where now  = startTime state
            time = (fromIntegral usecs / 1000000) `addUTCTime` now

    Nothing -> return endOfTime  -- XXX this is a sad wart in need of remedy

  checkQueuedTaskLimit

  resumeTMVar <- liftSTM newEmptyTMVar
  task <- asks task

  let wake value = do
        now <- getCurrentTime
        atomically $ putTMVar resumeTMVar (now, value)

      task' = task {
          taskStatus = Suspended (Wake wake)
        , taskState  = state { startTime = estimatedWakeup }
        }

  case maybeMicrosecs of
    Just usecs -> delayIO $ void $ forkIO $ delay usecs >> wake zero
    Nothing    -> return ()

  putTask task'

  callCC $ interrupt . Suspend . Resume
  (now, value) <- liftSTM $ takeTMVar resumeTMVar

  putTask task' { taskStatus = Running }

  resetLimits False
  modify $ \state -> state { startTime = now }

  case value of
    Err error -> raise error
    _         -> return value

bf_resume = Builtin "resume" 1 (Just 2)
            [TInt, TAny] TAny $ \(Int task_id : optional) -> do
  let [value] = defaults optional [zero]

  maybeTask <- getTask (fromIntegral task_id)
  case maybeTask of
    Just task@Task { taskStatus = Suspended (Wake wake) } -> do
      checkPermission (taskOwner task)
      putTask task { taskStatus = Running }
      delayIO (wake value)
    _ -> raise E_INVARG

  return zero

bf_queue_info = Builtin "queue_info" 0 (Just 1) [TObj] TAny $ \args ->
  let info = case args of
        []           -> objectList . S.toList .
                        foldr (S.insert . taskOwner) S.empty
        [Obj player] -> Int . fromIntegral . length .
                        filter ((== player) . taskOwner)
  in info <$> queuedTasks

bf_queued_tasks = Builtin "queued_tasks" 0 (Just 0) [] TLst $ \[] -> do
  tasks <- queuedTasks
  programmer <- frame permissions
  wizard <- isWizard programmer
  let ownedTasks | wizard    = tasks
                 | otherwise = filter ((== programmer) . taskOwner) tasks

  return $ fromListBy formatTask $ sort ownedTasks

  where formatTask :: Task -> Value
        formatTask task = fromListBy ($ task) [
            Int . fromIntegral . taskId        -- task-id
          , Int . floor . utcTimeToPOSIXSeconds .
                  startTime . taskState        -- start-time
          , const (Int 0)                      -- clock-id    (obsolete)
          , const (Int defaultBgTicks)         -- clock-ticks (obsolete)
          , Obj . taskOwner                    -- programmer
          , Obj . verbLocation . activeFrame   -- verb-loc
          , Str . verbFullName . activeFrame   -- verb-name
          , Int . fromIntegral .
                  lineNumber   . activeFrame   -- line
          , Obj . initialThis  . activeFrame   -- this
          , Int . fromIntegral . storageBytes  -- task-size
          ]

bf_kill_task = Builtin "kill_task" 1 (Just 1) [TInt] TAny $ \[Int task_id] -> do
  let task_id' = fromIntegral task_id
  maybeTask <- getTask task_id'

  case maybeTask of
    Just task@Task { taskStatus = status } | isQueued status -> do
      checkPermission (taskOwner task)
      purgeTask task
      delayIO $ killThread (taskThread task)
      return zero
    _ -> do
      thisTaskId <- taskId <$> asks task
      if task_id' == thisTaskId
        then interrupt Suicide
        else raise E_INVARG

bf_callers = Builtin "callers" 0 (Just 1) [TAny] TLst $ \optional -> do
  let [include_line_numbers] = booleanDefaults optional [False]

  Stack frames <- gets stack
  return $ formatFrames include_line_numbers (tail frames)

bf_task_stack = Builtin "task_stack" 1 (Just 2)
                [TInt, TAny] TLst $ \(Int task_id : optional) -> do
  let [include_line_numbers] = booleanDefaults optional [False]

  maybeTask <- getTask (fromIntegral task_id)
  case maybeTask of
    Just task@Task { taskStatus = Suspended{} } -> do
      checkPermission (taskOwner task)
      let Stack frames = stack $ taskState task
      return $ formatFrames include_line_numbers frames
    _ -> raise E_INVARG