-- |Netrium is Copyright Anthony Waite, Dave Hewett, Shaun Laurens & Contributors 2009-2018, and files herein are licensed
-- |under the MIT license,  the text of which can be found in license.txt
--
{-# LANGUAGE DeriveFunctor #-}

module Interpreter where

import Contract hiding (party)
import Observable (Steps(..), VarName)
import qualified Observable as Obs
import DecisionTree hiding (Trade)
import qualified DecisionTree as TD (DecisionStep(Trade))
import DecisionTreeSimplify
import Observations

import Prelude hiding (product, until, and)
import Data.List hiding (and)
import qualified Data.Map as Map
import Data.Map (Map)
import Control.Monad
import Text.XML.HaXml.Namespaces (localName)
import Text.XML.HaXml.Types (QName(..))
import Text.XML.HaXml.XmlContent hiding (next)
import XmlUtils
import Control.Exception (assert)


-- ---------------------------------------------------------------------------
-- * Main interpreter, using observables and choice data
-- ---------------------------------------------------------------------------

data Output = Trade Party Party Double Tradeable
            | OptionUntil   ChoiceId Time
            | OptionForever ChoiceId
  deriving (Eq, Show)

data StopReason = Finished                      -- ^ contract reduced to 'zero'
                | StoppedTime                   -- ^ stop time reached (in timeout mode)
                | StoppedWait                   -- ^ stopped at first wait point (in wait mode)
                | WaitForever                   -- ^ a non-terminating wait
                | ChoiceRequired       Party ChoiceId
                | ObservationExhausted VarName
                | ObservationMissing   VarName  -- ^ really an error
  deriving (Eq, Show)

data SimEnv
   = SimEnv {
       valueObservations :: Observations Double, -- ^ primitive real-valued obs
       condObservations  :: Observations Bool,   -- ^ primitive bool-valued obs
       optionsTaken      :: Choices (),          -- ^ 'anytime' options taken
       choicesMade       :: Choices Bool         -- ^ 'or' choices made
     }

data SimOutputs
   = SimOutputs {
       simTrace        :: TimedEvents String,
       simOutputs      :: TimedEvents Output,
       simStopReason   :: StopReason,
       simStopTime     :: Time,
       simStopContract :: Contract,
       simStopState    :: ProcessState,
       simStopWaitInfo :: Maybe WaitInfo
     }
  deriving (Show, Eq)

data StopWait = NoStop | StopFirstWait | StopNextWait
  deriving (Show, Eq)

data WaitInfo
   = WaitInfo {
       waitObs     :: [Obs Bool],
       waitHorizon :: Maybe Time,
       waitOptions :: [ChoiceId]
     }
  deriving (Show, Eq)

runContract :: SimEnv
            -> Time                -- ^ start time
            -> Maybe Time          -- ^ optional stop time
            -> StopWait            -- ^ stop at first waitpoint
            -> Either Contract ProcessState
            -> SimOutputs
runContract _ startTime (Just stopTime) _ _
  | not (stopTime > startTime)
  = error "runContract: stop time must be after start time"

runContract simenv startTime mStopTime mStopWait0 startState =
    let st0 = case startState of
                Left contract -> initialProcessState startTime contract
                Right st@(PSt time' _ _)
                  | startTime == time' -> st
                  | otherwise          -> error $ "runContract: resuming from the wrong time "
                                               ++ show time' ++ " vs " ++ show startTime

     in go [] [] mStopWait0 st0

  where
    go :: [(Time, String)] -> [(Time, Output)] -> StopWait
       -> ProcessState -> SimOutputs
    go trace output mStopWait st@(PSt time _ _) =
      -- if we go past the stop time, we've done something wrong...
      assert (maybe True (time <) mStopTime) $

      let obsenv = currentObsEnv (valueObservations simenv)
                                 (condObservations  simenv) time
          result = result' output
          result' out reason time' st' =
                   SimOutputs {
                     simTrace        = TEs (reverse trace),
                     simOutputs      = TEs (reverse out),
                     simStopReason   = reason,
                     simStopTime     = time',
                     simStopContract = currentContract st',
                     simStopState    = st',
                     simStopWaitInfo = Nothing
                   }
          step   = decisionStep st
          trace' = (time, show step) : trace
      in
      case decisionStep st of
        Done ->
          result Finished time st

        TD.Trade dir sf t next ->
          go trace' ((time, Trade p p' sf t) : output) mStopWait next
          where
            (p, p') = tradeDirParties dir

        Choose p cid next1 next2 ->
          case lookupChoice (choicesMade simenv) cid time of
            Nothing            -> result (ChoiceRequired p cid) time st
            Just v | v         -> go trace' output mStopWait next1
                   | otherwise -> go trace' output mStopWait next2

        ObserveCond obs next1 next2 ->
          case evalObs obsenv time obs of
            ObsExhausted varname    -> result (ObservationExhausted varname) time st
            ObsMissing   varname    -> result (ObservationMissing   varname) time st
            ObsResult v | v         -> go trace' output mStopWait next1
                        | otherwise -> go trace' output mStopWait next2

        ObserveValue obs next ->
          case evalObs obsenv time obs of
            ObsExhausted varname -> result (ObservationExhausted varname) time st
            ObsMissing   varname -> result (ObservationMissing   varname) time st
            ObsResult    v       -> go trace' output mStopWait (next v)

        Wait obsExprs optionsAvail | mStopWait == StopFirstWait ->
          case simplifyWait time obsExprs (not (null optionsAvail)) of
            Left  next   -> go trace' output mStopWait next
            Right []     -> result Finished time st
            Right conds' -> (result StoppedWait time st) { 
                              simStopWaitInfo = Just WaitInfo {
                                waitObs     = fmap fst conds',
                                waitHorizon = fmap fst (Obs.earliestTimeHorizon time conds'),
                                waitOptions = fmap fst optionsAvail
                              }
                            }

        Wait obsExprs optionsAvail ->

          let (time', waitResult) = runWait simenv obsenv
                                            mStopTime time
                                            obsExprs optionsAvail
          in case waitResult of
            ObsResult waitreason ->
                case waitreason of
                  WaitContinue next  -> go trace' outputU' mStopWait' (next time')
                  WaitStopped        -> result'   outputU' StoppedTime time' st
                  WaitFinished       -> result'   outputF' Finished    time' st
                  WaitNonTerm        -> result'   outputF' WaitForever time' st
              where
                outputU' = [ (time, OptionUntil choiceid time')
                           | (choiceid, _k) <- optionsAvail ] ++ output
                outputF' = [ (time, OptionForever choiceid)
                           | (choiceid, _k) <- optionsAvail ] ++ output
                mStopWait' | mStopWait == StopNextWait = StopFirstWait
                           | otherwise                 = mStopWait

            ObsExhausted varname -> result (ObservationExhausted varname) time' st
            ObsMissing   varname -> result (ObservationMissing   varname) time' st

data WaitResult k = WaitContinue k
                  | WaitStopped
                  | WaitFinished
                  | WaitNonTerm

runWait :: SimEnv
        -> ObsEnv
        -> Maybe Time
        -> Time
        -> [(Obs Bool, k)]
        -> [(ChoiceId, k)]
        -> (Time, ObsResult (WaitResult k))
runWait simenv obsenv mStopTime time obsExprs optionsAvail =
    checkEvents time (unTEs events)

  where
    timeouts = (case Obs.earliestTimeHorizon time obsExprs of
                 Nothing         -> []
                 Just (time', k) -> [(time', Just k)])
            ++ (case mStopTime of
                 Nothing         -> []
                 Just stopTime   -> [(stopTime, Nothing)])
    --events :: TimedEvents (WaitEvent k)
    events = mergeWaitEvents
               (valueObservations simenv) (condObservations simenv)
               (optionsTaken simenv)
               timeouts time obsenv

    -- Did we reach the time horizon?
    checkEvents time' [] = (time', ObsResult WaitNonTerm)
    checkEvents _ ((time', Timeout (Just k)):_remaining) =
      (time', ObsResult (WaitContinue k))

    checkEvents _ ((time', Timeout Nothing):_remaining) =
      (time', ObsResult WaitStopped)

    -- Check if we took an available option
    checkEvents _ ((time', TakeOption cid):remaining) =
      case lookup cid optionsAvail of
        Just k  -> (time', ObsResult (WaitContinue k))
        Nothing -> checkEvents time' remaining

    -- Check if any observable is true at this time
    checkEvents _ ((time', ObsChanged obsEnv): remaining) =
        case foldr accum (ObsResult Nothing) obsExprs of
          ObsResult (Just k)   -> (time', ObsResult (WaitContinue k))
          ObsResult Nothing
            | all (Obs.evermoreFalse time' . fst) obsExprs
                        -> (time', ObsResult WaitFinished)
            | otherwise -> checkEvents time' remaining
          ObsExhausted varname -> (time', ObsExhausted varname)
          ObsMissing   varname -> (time', ObsExhausted varname)

      where
        accum (obs, k) rest =
          case evalObs obsEnv time' obs of
            ObsResult    True    -> ObsResult (Just k)
            ObsResult    False   -> rest
            ObsExhausted varname -> ObsExhausted varname
            ObsMissing   varname -> ObsMissing   varname


-- | When in a wait state there are three different things that can happen
-- one of the observables can become true, we can choose to take an 'anytime'
-- option that is available to us.
--
-- There are two ways an observable can become true, one is due to a change in
-- a primitive/external ovservable, and the other is via the passage of time.
--
-- Hence, overall, there are three events we are interested in while waiting.
--
data WaitEvent k = TakeOption ChoiceId
                 | ObsChanged ObsEnv
                 | Timeout    k
  deriving Show

-- | Take all three sources of events we are interested in and produce a
-- unified event list
--
mergeWaitEvents :: Observations Double  -- ^ time series for real primitive obs
                -> Observations Bool    -- ^ time series for bool primitive obs
                -> Choices ()           -- ^ 'anytime' options taken
                -> [(Time, k)]          -- ^ optional timeouts
                -> Time                 -- ^ initial time
                -> ObsEnv               -- ^ initial values of all primitive obs
                -> TimedEvents (WaitEvent k)
mergeWaitEvents valObss condObss options timeouts time0 obsenv0 =
    events'
  where

    -- Firstly, combine all the observations into a unified event list
    obsTS :: TimedEvents [(VarName, Either (Maybe Double) (Maybe Bool))]
    obsTS = mconcat (valTSs ++ condTSs)

    valTSs, condTSs :: [TimedEvents [(VarName, Either (Maybe Double) (Maybe Bool))]]

    valTSs  = [ fmap (\e -> [(varname, Left e)])
                     (pruneTimedEvents time0 (timeSeriesEvents ts))
              | (varname, ts) <- Map.toList valObss ]

    condTSs = [ fmap (\v -> [(varname, Right v)])
                     (pruneTimedEvents time0 (timeSeriesEvents ts))
              | (varname, ts) <- Map.toList condObss ]

    -- similarly for the options, a unified event list
    optionsTS :: TimedEvents ChoiceId
    optionsTS = mconcat
                [ fmap (const cid) (pruneTimedEvents time0 ts)
                | (cid, ts) <- Map.toList options ]

    -- for the observations, convert the list of changes in observations
    -- into a list of ObsEnv values at each time
    obsEnvTS :: TimedEvents ObsEnv
    obsEnvTS = insertEventBefore time0 obsenv0
             $ snd (mapAccumTS accumObsEnv obsenv0 obsTS)

    accumObsEnv :: ObsEnv
                -> [(VarName, Either (Maybe Double) (Maybe Bool))]
                -> (ObsEnv, ObsEnv)
    accumObsEnv obsenv obschanges = (obsenv', obsenv')
      where
        obsenv' = foldl' update obsenv obschanges

        update (ObsEnv realObsvns boolObsvns) (varname, Left  v) =
            ObsEnv realObsvns' boolObsvns
          where
            realObsvns' = Map.insert varname v realObsvns

        update (ObsEnv realObsvns boolObsvns) (varname, Right v) =
            ObsEnv realObsvns boolObsvns'
          where
            boolObsvns' = Map.insert varname v boolObsvns

    -- Now combine the different events into one event list
    -- firstly the observations and the options
    events  = mergeEventsBiased (fmap ObsChanged obsEnvTS)
                                (fmap TakeOption optionsTS)

    -- and lastly any timeouts
    events' = foldr (\(time, k) -> insertEventAfter time (Timeout k))
                    events timeouts


-- ---------------------------------------------------------------------------
-- * Evaluating observables in the presense of observables data
-- ---------------------------------------------------------------------------

data ObsEnv = ObsEnv (Map VarName (Maybe Double))
                     (Map VarName (Maybe Bool))
  deriving Show

currentObsEnv :: Observations Double
              -> Observations Bool
              -> Time
              -> ObsEnv
currentObsEnv realObsvns boolObsvns time =
    ObsEnv (fmap (flip lookupTimeSeries time) realObsvns)
           (fmap (flip lookupTimeSeries time) boolObsvns)


data ObsResult a = ObsResult a
                 | ObsExhausted VarName
                 | ObsMissing   VarName
  deriving (Functor, Show)

evalObs :: ObsEnv
        -> Time
        -> Obs a
        -> ObsResult a
evalObs (ObsEnv realObsvns boolObsvns) time =
    go . Obs.eval time
  where
    go :: Steps a -> ObsResult a
    go (Result v) = ObsResult v

    go (NeedNamedVal _ varname k) =
      case Map.lookup varname realObsvns of
        Nothing       -> ObsMissing varname
        Just Nothing  -> ObsExhausted varname
        Just (Just v) -> go (k v)

    go (NeedNamedCond _ varname k) =
      case Map.lookup varname boolObsvns of
        Nothing       -> ObsMissing varname
        Just Nothing  -> ObsExhausted varname
        Just (Just v) -> go (k v)


-- ---------------------------------------------------------------------------
-- * XML instances
-- ---------------------------------------------------------------------------

instance HTypeable Output where
    toHType _ = Defined "Output" [] []

instance XmlContent Output where
    parseContents = do
      e@(Elem t _ _) <- element ["Trade","OptionUntil","OptionForever"]
      commit $ interior e $ case localName t of
        "Trade"         -> liftM4 Trade parseContents parseContents
                                        parseContents parseContents
        "OptionUntil"   -> liftM2 OptionUntil   (attrStr (N "choiceid") e) parseContents
        "OptionForever" -> liftM  OptionForever (attrStr (N "choiceid") e)
        _               -> fail "cannot parse"

    toContents (Trade p p' sf t)       = [mkElemC "Trade" (toContents p
                                                          ++ toContents p'
                                                          ++ toContents sf
                                                          ++ toContents t)]
    toContents (OptionUntil cid time') = [mkElemAC (N "OptionUntil")
                                                   [(N "choiceid", str2attr cid)]
                                                   (toContents time')]
    toContents (OptionForever cid)     = [mkElemAC (N "OptionForever")
                                                   [(N "choiceid", str2attr cid)] []]


instance HTypeable StopReason where
    toHType _ = Defined "StopReason" [] []

instance XmlContent StopReason where
    parseContents = do
      e@(Elem t _ _) <- element ["Finished", "StoppedTime", "StoppedWait","WaitForever"
                                ,"ChoiceRequired"
                                ,"ObservationMissing","ObservationExhausted"]
      commit $ interior e $ case localName t of
        "Finished"       -> return Finished
        "StoppedTime"    -> return StoppedTime
        "StoppedWait"    -> return StoppedWait
        "WaitForever"    -> return WaitForever
        "ChoiceRequired" -> liftM2 ChoiceRequired parseContents
                                                  (attrStr (N "choiceid") e)
        "ObservationMissing"   -> liftM ObservationMissing   (attrStr (N "var") e)
        "ObservationExhausted" -> liftM ObservationExhausted (attrStr (N "var") e)
        _                      -> fail "cannot parse"

    toContents Finished    = [mkElemC "Finished"    []]
    toContents StoppedTime = [mkElemC "StoppedTime" []]
    toContents StoppedWait = [mkElemC "StoppedWait" []]
    toContents WaitForever = [mkElemC "WaitForever" []]

    toContents (ChoiceRequired party choiceid) =
        [mkElemAC (N "ChoiceRequired") [(N "choiceid", str2attr choiceid)]
                                       (toContents party)]
    toContents (ObservationExhausted varname) =
        [mkElemAC (N "ObservationExhausted") [(N "var", str2attr varname)] []]
    toContents (ObservationMissing   varname) =
        [mkElemAC (N "ObservationMissing") [(N "var", str2attr varname)] []]


instance HTypeable StopWait where
    toHType _ = Defined "StopWait" [] []

instance XmlContent StopWait where
  parseContents = (do
    e@(Elem t _ _) <- element ["StopFirstWait", "StopNextWait"]
    commit $ interior e $ case localName t of
      "StopFirstWait" -> return StopFirstWait
      "StopNextWait"  -> return StopNextWait
      _               -> fail "cannot parse")
    `onFail` return NoStop

  toContents NoStop        = []
  toContents StopFirstWait = [mkElemC "StopFirstWait" []]
  toContents StopNextWait  = [mkElemC "StopNextWait" []]


instance HTypeable WaitInfo where
    toHType _ = Defined "WaitInfo" [] []

instance XmlContent WaitInfo where
  parseContents = inElement "WaitInfo" $ do
                    obss <- parseContents
                    t    <- parseContents
                    opts <- parseContents
                    return $ WaitInfo ((map (\(WaitCondition obs) -> obs)) obss)
                                      t
                                      (map (\(WaitOption cid) -> cid) opts)
  toContents (WaitInfo obss t opts) = [mkElemC "WaitInfo" (toContents (map WaitCondition obss)
                                                        ++ toContents t
                                                        ++ toContents (map WaitOption opts))]

newtype WaitCondition = WaitCondition (Obs Bool)

instance HTypeable WaitCondition where
    toHType _ = Defined "WaitCondition" [] []

instance XmlContent WaitCondition where
  parseContents = inElement "WaitCondition" $
                    liftM WaitCondition Obs.parseObsCond
  toContents (WaitCondition obs) = [mkElemC "WaitCondition" [Obs.printObs obs]]


newtype WaitOption = WaitOption ChoiceId

instance HTypeable WaitOption where
    toHType _ = Defined "WaitOption" [] []

instance XmlContent WaitOption where
  parseContents = inElement "WaitOption" $
                    liftM WaitOption text
  toContents (WaitOption cid) = [mkElemC "WaitOption" (toText cid)]
