-----------------------------------------------------------------------------
-- Copyright 2019, Advise-Me project team. This file is distributed under 
-- the terms of the Apache License 2.0. For more information, see the files
-- "LICENSE.txt" and "NOTICE.txt", which are included in the distribution.
-----------------------------------------------------------------------------
-- |
-- Maintainer  :  bastiaan.heeren@ou.nl
-- Stability   :  provisional
-- Portability :  portable (depends on ghc)
--
-----------------------------------------------------------------------------

module Bayes.Script
   ( taskConcepts
   , emptyConcepts
   , aggregateConcepts
   , aggregateConcepts'
   , findEvidence
   , addModelToLatestRequest
   ) where

import Bayes.Evidence
import Bayes.EliminationOrdering ( minFactorOrder )
import Bayes.Inference ( infer, toEvidence )
import Bayes.Network
import Data.List hiding (insert)
import Data.Maybe
import Data.Function ( on )
import Control.Monad ( (>=>) )
import Database.HDBC
import Database.HDBC.Sqlite3
import Ideas.Common.Id ( showId )
import qualified Ideas.Text.XML as XML ( fromXML, XML, parseXML, findChild )
--import qualified Ideas.Text.XML.Interface as XML
import qualified Data.Map as M
import qualified Data.Set as S

import qualified Task.Network.StudentModel as StudentModel
import Recognize.Data.MathStoryProblem (Task(Task), singleNetwork)
import Bayes.Probability
import Database.Data
import Main.Tasks ( tasks, findTaskFuzzy )


-- | Add information from the student modeller to the database, based on the
-- raw input and output of the domain reasoner. This is unfortunately done in a
-- roundabout way, due to the way the framework functions.
--
-- We assume that the latest row to the requests table is the one we're dealing
-- with, since we must know the row ID to continue.
addModelToLatestRequest :: Connection -> IO ()
addModelToLatestRequest conn = do
   r <- fromMaybe (error "No request to add model to.") <$> latestRecord conn
   let xmlInput = either (error "Could not read XML") id . XML.parseXML $ r ! "input"
       (sID, tID) = fromMaybe mempty . collectIDs $ xmlInput
       inputIDs = intercalate "," . map fst . fromMaybe mempty . collectInputs $ xmlInput
       reqnr = r ! "rowid" :: Int
   ev <- findEvidence $ r ! "output"
   let partialModel = taskConcepts tID ev
   partialModels <- latestPartialModels conn sID
   let studentModel = aggregateConcepts $ (tID, partialModel) : partialModels
   insertRecord conn studentsTable
       ( insert "studentid" sID
       . insert "taskid" tID
       . insert "inputs" inputIDs
       . insert "requestnr" reqnr
       . insert "evidence" ev
       . insert "studentmodel" studentModel
       . insert "partialmodel" partialModel
       $ mempty )
   commit conn

-- Find evidence from a reply of the domain reasoner.
findEvidence :: Monad m => XML.XML -> m Evidence
findEvidence = XML.findChild "diagnosis" >=> XML.findChild "evidence" >=> XML.fromXML


-- | Run the evidence on the Bayesian networks to add soft evidence.
--
-- Instead of running it on the combined model, we run it through the models
-- seperately and only output the evidence as it should be in the student model
-- nodes. This should work since there should be no feedback effects at that
-- point.
--feed :: Evidence -> StudentModel
--feed = toEvidence StudentModel.network . feed'


-- | Obtain task concept node probabilities for a particular task.
taskConcepts :: TaskID -> Evidence -> Evidence
taskConcepts tID ev =
   let nw = maybe (error $ "no such task " ++ tID) (\(Task t) -> singleNetwork t) $ findTaskFuzzy tID
   in  filterEvidence isConcept . toEvidence nw $ infer nw ev minFactorOrder (S.filter isConcept (nodeIds nw))


-- | Feeding empty evidence to the task concept evidence. This is used in case
-- the task is not even mentioned in the precalculations (presumably because no
-- request for the task was sent).
emptyConcepts :: [ (TaskID, Evidence) ]
emptyConcepts = flip map tasks $ \t -> let tID = showId t in (tID, taskConcepts tID mempty)

-- | Same as 'aggregateConcepts', but do not include task concept node
-- probabilities in final evidence.
aggregateConcepts' :: [ (TaskID, Evidence) ] -> Evidence
aggregateConcepts' precalculatedTaskConcepts
   = toEvidence StudentModel.network
   . M.map ((\x->[x,1-x]) . bayesAverage)
   . M.fromListWith (++) $
      [ (concept, [fromDouble . fromRational $ probability])
      | Task t <- tasks
      , let taskConcepts' = fromJust . lookup (showId t) $ precalculatedTaskConcepts ++ emptyConcepts
      , (taskConcept, stateLabel, probability) <- allProbabilities taskConcepts'
      , stateLabel == "Yes" || stateLabel == "State0" || stateLabel == "Correct" || stateLabel == "Correctr"
      , concept <- maybeToList . getConcept $ taskConcept
      ]


-- | Aggregate task concept node probabilities into generic concept node
-- probabilities.
aggregateConcepts :: [ (TaskID, Evidence) ] -> Evidence
aggregateConcepts p = aggregateConcepts' p `mappend` mconcat (map snd . nubBy ((==) `on` fst) $ p ++ emptyConcepts)


-- for now, just use plain average
bayesAverage :: [Probability] -> Probability
bayesAverage ps = sum ps / fromIntegral (length ps)

-- | Task model concept node ID to a student model concept node ID.
getConcept :: String -> Maybe String
getConcept ct =
   let cs = takeWhile (/= '_') $ ct
   in if cs `S.member` nodeIds StudentModel.network
         then Just cs
         else Nothing

isConcept :: String -> Bool
isConcept = isJust . getConcept