# Copyright 2014 VMware, Inc.  All rights reserved. -- VMware Confidential
#
'''Polling aggregator that continuously updates the public status
   file with latest information it aggregates.
'''

import json
import logging
import os
import sys
import traceback
import codecs
import datetime

from . import json_utils
from .filelock import SecureOpen
from threading import Timer, RLock, Event
from .componentStatus import ReplyInfo, ExecutionStatusInfo

TIME_PATTERN = "%Y-%m-%dT%H:%M:%S.%f"

class StatusAggregator(object):
   def __init__(self, interval, outFile, progressFun,
                inputFun=None, replyHandler=None):
      '''Constructor

      @param interval: the periodically polling interval
      @type interval: integer in seconds

      @param outFile: the public output file
      @type outFile: string

      @param progressFun: the functor to aggregate the progress status
      @type progressFun: function object

      @param inputFun: the functor to acquire the raw progress status,
         can be none to use local file.
      @type inputFun: function object, returns ComponentsExecutionStatusInfo

      @param replyHandler: the functor to handle user replies. Can be
         none to use local file.
      @type replyHandler: function object that updates internal status
         with the reply.
      '''
      self._timer = None
      self.interval = interval
      self._output = outFile
      self._progressFun = progressFun

      self._inputFun = inputFun
      self._replyHandler = replyHandler

      # tracking the state
      self._lock = RLock()
      self._cancelled = False
      self._started = False
      self._canFlush = Event()

      # pending question
      self._question = None

      self.start_time = None
      self.end_time = None

      # Keep last published status
      self.publishedStatus = None

   def _doWork(self):
      ''' Status-aggregator internal function, do not call directly.
      '''
      self._canFlush.clear()

      try:
         # get raw status from internal repository
         rawStatus = self._inputFun()

         # if there is an existing pending question
         if self._question:
            rfile = self._question.reply_file
            qid = self._question.id

            logging.debug("Checking reply")
            # if question already handled, clear the reply
            if not rawStatus.question:
               try:
                  logging.info("Question handled, removing reply %s" % rfile)
                  os.remove(rfile)
               except Exception:
                  pass
               finally:
                  self._question = None
            # otherwise, check for replies
            elif os.path.exists(rfile):
               logging.debug("Found reply file %s" % rfile)
               with open(rfile, 'r') as fp:
                  fileCnt = fp.read()
                  replyDict = json_utils.JsonSerializer().deserialize(fileCnt)
                  reply = ReplyInfo(**replyDict)

                  if reply.reply_to != qid:
                     raise SystemError("question answer doesn't match")

                  # update components about the reply
                  self._replyHandler(reply)
         # set pending question if got a new question
         elif rawStatus.question:
            self._question = rawStatus.question
            logging.debug("Found question %s" % self._question.id)

         # get overall progress
         overallProgress = self._progressFun(rawStatus)

         # compose public status file
         publicStatus = ExecutionStatusInfo(info=rawStatus.info,
                                            warning=rawStatus.warning,
                                            error=rawStatus.error,
                                            question=rawStatus.question,
                                            progress=overallProgress,
                                            start_time=self.start_time,
                                            end_time=self.end_time)

         if self.publishedStatus != publicStatus:
            # Securely dump status to the output file. If client reads the
            # output file in the same moment it won't be affected.
            with SecureOpen(self._output, codecs.open, True, "w",
                            encoding='utf-8') as fp:
                  # Dump to output file only if there is a delta
               json.dump(publicStatus, fp,
                         default=json_utils.ObjectJsonConverter(False).encode,
                         indent=4)
            self.publishedStatus = publicStatus
      except Exception:
         logging.exception("Error in aggregator:")
      finally:
         self._canFlush.set()

      with self._lock:
         if not self._cancelled:
            self._timer = Timer(self.interval, self._doWork)
            self._timer.start()

   def start(self):
      ''' Start the aggregator. If has already been started then do nothing'''
      with self._lock:
         if self._started:
            return
         self._started = True
         self.start_time = "%sZ" % datetime.datetime.utcnow().strftime(TIME_PATTERN)[:-3]

      self._doWork()

   def stop(self):
      '''Stop the timer but run aggregator one last time to get the lastest
      aggregation.
      '''
      with self._lock:
         if self._started:
            logging.warning("stopping status aggregation...")
            self.end_time = "%sZ" % datetime.datetime.utcnow().strftime(TIME_PATTERN)[:-3]
            self._cancelled = True
            self._timer.cancel()
            self._started = False
            self._canFlush.wait()
            self._doWork()
