""" a scheduler of threads, of course!
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

__RCSID__ = "$Id$"

import hashlib
import threading
import time

from DIRAC import S_ERROR, S_OK, gLogger
from DIRAC.Core.Utilities.ThreadSafe import Synchronizer

gSchedulerLock = Synchronizer()


class ThreadScheduler(object):

  def __init__(self, enableReactorThread=True, minPeriod=60):
    self.__thId = False
    self.__minPeriod = minPeriod
    self.__taskDict = {}
    self.__hood = []
    self.__createReactorThread = enableReactorThread
    self.__nowEpoch = time.time
    self.__sleeper = time.sleep
    self.__min = min

  def setMinValidPeriod(self, period):
    self.__minPeriod = period

  def disableCreateReactorThread(self):
    self.__createReactorThread = False

  def addPeriodicTask(self, period, taskFunc, taskArgs=(), executions=0, elapsedTime=0):
    if not callable(taskFunc):
      return S_ERROR("%s is not callable" % str(taskFunc))
    period = max(period, self.__minPeriod)
    elapsedTime = min(elapsedTime, period - 1)
    md = hashlib.md5()
    task = {'period': period,
            'func': taskFunc,
            'args': taskArgs,
            }
    md.update(str(task).encode())
    taskId = md.hexdigest()
    if taskId in self.__taskDict:
      return S_ERROR("Task %s is already added" % taskId)
    if executions:
      task['executions'] = executions
    self.__taskDict[taskId] = task
    retVal = self.__scheduleTask(taskId, elapsedTime)
    if not retVal['OK']:
      return retVal
    self.__createExecutorIfNeeded()
    return S_OK(taskId)

  def setTaskPeriod(self, taskId, period):
    try:
      period = int(period)
    except ValueError:
      return S_ERROR("Period must be a number")
    period = max(period, self.__minPeriod)
    try:
      self.__taskDict[taskId]['period'] = period
    except KeyError:
      return S_ERROR("Unknown task %s" % taskId)
    return S_OK()

  @gSchedulerLock
  def removeTask(self, taskId):
    if taskId not in self.__taskDict:
      return S_ERROR("Task %s does not exist" % taskId)
    del(self.__taskDict[taskId])
    for i in range(len(self.__hood)):
      if self.__hood[i][0] == taskId:
        del(self.__hood[i])
        break
    return S_OK()

  def addSingleTask(self, taskFunc, taskArgs=()):
    return self.addPeriodicTask(self.__minPeriod,
                                taskFunc,
                                taskArgs,
                                executions=1,
                                elapsedTime=self.__minPeriod)

  @gSchedulerLock
  def __scheduleTask(self, taskId, elapsedTime=0):
    executeInSecs = self.__taskDict[taskId]['period']
    elapsedTime = min(elapsedTime, executeInSecs - 1)
    if elapsedTime:
      executeInSecs -= elapsedTime

    now = time.time()
    for i in range(len(self.__hood)):
      if taskId == self.__hood[i][0]:
        tD = self.__hood[i][1] - now
        if abs(tD - executeInSecs) < 30:
          return S_OK()
        else:
          del(self.__hood[i])
          break

    executionTime = now + executeInSecs
    inserted = False
    for i in range(len(self.__hood)):
      if executionTime < self.__hood[i][1]:
        self.__hood.insert(i, (taskId, executionTime))
        inserted = True
        break
    if not inserted:
      self.__hood.append((taskId, executionTime))

    return S_OK()

  def __executorThread(self):
    while self.__hood:
      timeToNext = self.executeNextTask()
      if timeToNext is None:
        break
      if timeToNext and timeToNext > 0.1:
        self.__sleeper(self.__min(timeToNext, 1))
    # If we are leaving
    self.__destroyExecutor()

  def executeNextTask(self):
    if not self.__hood:
      return None
    timeToWait = self.__timeToNextTask()
    if timeToWait and timeToWait > 0:
      return timeToWait
    taskId = self.__popNextTaskId()
    startTime = time.time()
    self.__executeTask(taskId)
    elapsedTime = time.time() - startTime
    self.__scheduleIfNeeded(taskId, elapsedTime)
    return self.__timeToNextTask()

  @gSchedulerLock
  def __createExecutorIfNeeded(self):
    if not self.__createReactorThread:
      return
    if self.__thId:
      return
    self.__thId = threading.Thread(target=self.__executorThread)
    self.__thId.setDaemon(True)
    self.__thId.start()

  @gSchedulerLock
  def __destroyExecutor(self):
    self.__thId = False

  @gSchedulerLock
  def __timeToNextTask(self):
    if not self.__hood:
      return None
    return self.__hood[0][1] - self.__nowEpoch()

  @gSchedulerLock
  def __popNextTaskId(self):
    if len(self.__hood) == 0:
      return None
    return self.__hood.pop(0)[0]

  @gSchedulerLock
  def getNextTaskId(self):
    if len(self.__hood) == 0:
      return None
    return self.__hood[0][0]

  @gSchedulerLock
  def setNumExecutionsForTask(self, taskId, numExecutions):
    if taskId not in self.__taskDict:
      return False
    if numExecutions:
      self.__taskDict[taskId]['executions'] = numExecutions
    else:
      del(self.__taskDict[taskId]['executions'])

  def __executeTask(self, taskId):
    if taskId not in self.__taskDict:
      return False
    task = self.__taskDict[taskId]
    if 'executions' in task:
      task['executions'] -= 1
    try:
      task['func'](*task['args'])
    except Exception as lException:
      gLogger.exception("Exception while executing scheduled task", lException=lException)
      return False
    return True

  def __scheduleIfNeeded(self, taskId, elapsedTime=0):
    if 'executions' in self.__taskDict[taskId]:
      if self.__taskDict[taskId]['executions'] == 0:
        del(self.__taskDict[taskId])
        return True
    return self.__scheduleTask(taskId, elapsedTime)


gThreadScheduler = ThreadScheduler()
