"""
It used to create several plots
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function
import time
import copy

from DIRAC import S_OK, S_ERROR, gLogger
from DIRAC.Core.Utilities.Plotting import gDataCache
from DIRAC.Core.Utilities.Plotting.Plots import generateNoDataPlot, generateTimedStackedBarPlot, generateQualityPlot,\
    generateCumulativePlot, generatePiePlot, generateStackedLinePlot

from DIRAC.MonitoringSystem.private.DBUtils import DBUtils

__RCSID__ = "$Id$"


class BasePlotter(DBUtils):

  _EA_THUMBNAIL = 'thumbnail'
  _EA_WIDTH = 'width'
  _EA_HEIGHT = 'height'
  _EA_THB_WIDTH = 'thbWidth'
  _EA_THB_HEIGHT = 'thbHeight'
  _EA_PADDING = 'figurePadding'
  _EA_TITLE = 'plotTitle'

  _RATE_UNITS = {
      'time': (
          ('seconds / s',
           1,
           24),
          ('hours / s',
           3600,
           24),
          ('days / s',
           86400,
           15),
          ('weeks / s',
           86400 * 7,
           10),
          ('months / s',
           86400 * 30,
           12),
          ('years / s',
           86400 * 365,
           1)),
      'cpupower': (
          ('HS06',
           1,
           750),
          ('kHS06',
           1000,
           750),
          ('MHS06',
           10 ** 6,
           1)),
      'bytes': (
          ('MB / s',
           10 ** 6,
           1000),
          ('GB / s',
              10 ** 9,
              1000),
          ('TB / s',
              10 ** 12,
              1000),
          ('PB / s',
              10 ** 15,
              1)),
      'jobs': (
          ('jobs / hour',
           1 / 3600.0,
           1000),
          ('kjobs / hour',
           (10 ** 3) / 3600.0,
           1000),
          ('Mjobs / hour',
           (10 ** 6) / 3600.0,
           1)),
      'files': (
          ('files / hour',
           1 / 3600.0,
           1000),
          ('kfiles / hour',
           (10 ** 3) / 3600.0,
           1000),
          ('Mfiles / hour',
           (10 ** 6) / 3600.0,
           1))}

  _UNITS = {'time': (('seconds', 1, 24), ('hours', 3600, 24), ('days', 86400, 15), ('weeks', 86400 * 7, 10),
                     ('months', 86400 * 30, 12), ('years', 86400 * 365, 1)),
            'cpupower': (('HS06 hours', 3600, 24), ('HS06 days', 86400, 750), ('kHS06 days', 86400 * 1000, 750),
                         ('MHS06 days', 86400 * 10 ** 6, 1)),
            'bytes': (('MB', 10 ** 6, 1000), ('GB', 10 ** 9, 1000), ('TB', 10 ** 12, 1000), ('PB', 10 ** 15, 1)),
            'jobs': (('jobs', 1, 1000), ('kjobs', 10 ** 3, 1000), ('Mjobs', 10 ** 6, 1)),
            'files': (('files', 1, 1000), ('kfiles', 10 ** 3, 1000), ('Mfiles', 10 ** 6, 1))
            }

  # To be defined in the derived classes
  _typeKeyFields = []
  _typeName = ''

  def __init__(self, db, setup, extraArgs=None):
    super(BasePlotter, self).__init__(db, setup)
    """ c'tor

    :param self: self reference
    """

    if isinstance(extraArgs, dict):
      self._extraArgs = extraArgs
    else:
      self._extraArgs = {}
    reportsRevMap = {}
    for attr in dir(self):
      if attr.startswith("_report"):
        if attr.endswith('Name'):
          reportId = attr[7:-4]
          reportName = getattr(self, attr)
          reportsRevMap[reportId] = reportName
        else:
          reportId = attr[7:]
          if reportId not in reportsRevMap:
            reportsRevMap[reportId] = reportId
    self.__reportNameMapping = {}
    for rId in reportsRevMap:
      self.__reportNameMapping[reportsRevMap[rId]] = rId

  def generate(self, reportRequest):
    """
    It retrives the data from the database and create the plot

    :param dict reportRequest: contains the plot attributes
    """
    reportHash = reportRequest['hash']
    reportName = reportRequest['reportName']
    if reportName in self.__reportNameMapping:
      reportRequest['reportName'] = self.__reportNameMapping[reportName]

    gLogger.info("Retrieving data for %s:%s" % (
        reportRequest['typeName'], reportRequest['reportName']))
    sT = time.time()
    retVal = self.__retrieveReportData(reportRequest, reportHash)
    reportGenerationTime = time.time() - sT
    if not retVal['OK']:
      return retVal
    if not reportRequest['generatePlot']:
      return retVal
    reportData = retVal['Value']
    gLogger.info("Plotting data for %s:%s" % (
        reportRequest['typeName'], reportRequest['reportName']))
    sT = time.time()
    retVal = self.__generatePlotForReport(reportRequest, reportHash, reportData)
    plotGenerationTime = time.time() - sT
    gLogger.verbose(
        "Time for %s:%s - Report %.2f Plot %.2f (%.2f%% r/p)" %
        (reportRequest['typeName'],
         reportRequest['reportName'],
         reportGenerationTime,
         plotGenerationTime,
         ((reportGenerationTime * 100 / plotGenerationTime) if plotGenerationTime else 0.)))
    if not retVal['OK']:
      return retVal
    plotDict = retVal['Value']
    if 'retrieveReportData' in reportRequest['extraArgs'] and reportRequest['extraArgs']['retrieveReportData']:
      plotDict['reportData'] = reportData
    return S_OK(plotDict)

  def plotsList(self):
    """
    It returns the list of available plots.
    """
    return sorted(self.__reportNameMapping)

  def __retrieveReportData(self, reportRequest, reportHash):
    """
    It uses the appropriate Plotter to retrieve the data from the database.

    :param dict reportRequest: the dictionary which contains the conditions used to create the plot
    :param str reportHash: it is the unique identifier used to cache a plot
    :return: dict S_OK/S_ERROR if the data found in the cache it returns from it otherwise it uses the cache.
    """
    funcName = "_report%s" % reportRequest['reportName']
    if not hasattr(self, funcName):
      return S_ERROR("Report %s is not defined" % reportRequest['reportName'])
    else:
      funcObj = getattr(self, funcName)

    return gDataCache.getReportData(reportRequest=reportRequest,
                                    reportHash=reportHash,
                                    dataFunc=funcObj)

  def __generatePlotForReport(self, reportRequest, reportHash, reportData):
    """It creates the plot

    :param dict reportRequest: contains the plot attributes
    :param str reportHash: unique string which identify the plot
    :param dict repotData: contains the data used to generate the plot.
    """

    funcName = "_plot%s" % reportRequest['reportName']
    try:
      funcObj = getattr(self, funcName)
    except BaseException:
      return S_ERROR("Plot function for report %s is not defined" % reportRequest['reportName'])

    return gDataCache.getReportPlot(reportRequest=reportRequest,
                                    reportHash=reportHash,
                                    reportData=reportData,
                                    plotFunc=funcObj)

  def _getTimedData(self, startTime, endTime, selectField, preCondDict, metadataDict=None):
    """
    It retrieves the time series data from the ES database.

    :param int startTime: epoch time
    :param int endTime: epoch time
    :param str selectField: the value that we want to plot
    :param dict preCondDict: plot attributes
    :param dict metadataDict: extra arguments used to create the plot.

    """

    condDict = {}

    if metadataDict is None:
      metadataDict = {}

    grouping = preCondDict['grouping'][0]
    # Make safe selections
    for keyword in self._typeKeyFields:
      if keyword in preCondDict:
        condDict[keyword] = preCondDict[keyword]

    retVal = self._determineBucketSize(startTime, endTime)
    if not retVal['OK']:
      return retVal
    interval, granularity = retVal['Value']

    dynamicBucketing = metadataDict.get('DynamicBucketing', True)
    # by default we use dynamic bucketing
    if dynamicBucketing:
      retVal = self._retrieveBucketedData(self._typeName,
                                          startTime,
                                          endTime,
                                          interval,
                                          selectField,
                                          condDict,
                                          grouping,
                                          metadataDict)
    else:
      retVal = self._retrieveAggregatedData(self._typeName,
                                            startTime,
                                            endTime,
                                            interval,
                                            selectField,
                                            condDict,
                                            grouping,
                                            metadataDict)

    if not retVal['OK']:
      return retVal
    dataDict = retVal['Value']

    return S_OK((dataDict, granularity))

  def _getSummaryData(self, startTime, endTime, selectField, preCondDict, metadataDict=None):
    """
    It returns the data used to create the pie chart plot.

    :param int startTime: epoch time
    :param int endTime: epoch time
    :param str selectField: the value what we want to plot
    :param dict preCondDict: plot attributes
    :param dict metadataDict: extra arguments used to create the plot.
    """
    grouping = preCondDict['grouping'][0]
    condDict = {}
    # Make safe selections
    for keyword in self._typeKeyFields:
      if keyword in preCondDict:
        condDict[keyword] = preCondDict[keyword]

    retVal = self._determineBucketSize(startTime, endTime)
    if not retVal['OK']:
      return retVal
    interval, _ = retVal['Value']

    retVal = self._retrieveBucketedData(typeName=self._typeName,
                                        startTime=startTime,
                                        endTime=endTime,
                                        interval=interval,
                                        selectField=selectField,
                                        condDict=condDict,
                                        grouping=grouping,
                                        metadataDict=metadataDict)
    if not retVal['OK']:
      return retVal
    dataDict = retVal['Value']
    return S_OK(dataDict)

  def _findSuitableRateUnit(self, dataDict, maxValue, unit):
    """
    Returns the suitable unit for a given dataset.
    """
    return self._findUnitMagic(dataDict, maxValue, unit, self._RATE_UNITS)

  def _findSuitableUnit(self, dataDict, maxValue, unit):
    """
    Returns the suitable unit for a given dataset.
    """
    return self._findUnitMagic(dataDict, maxValue, unit, self._UNITS)

  def _findUnitMagic(self, reportDataDict, maxValue, unit, selectedUnits):
    """
    Returns the suitable unit for a given dataset.
    """
    if unit not in selectedUnits:
      raise AttributeError("%s is not a known rate unit" % unit)
    baseUnitData = selectedUnits[unit][0]
    if self._extraArgs.get('staticUnits'):
      unitData = selectedUnits[unit][0]
    else:
      unitList = selectedUnits[unit]
      unitIndex = -1
      for _, unitDivFactor, unitThreshold in unitList:
        unitIndex += 1
        if maxValue / unitDivFactor < unitThreshold:
          break
      unitData = selectedUnits[unit][unitIndex]
    # Apply divFactor to all units
    graphDataDict, maxValue = self._divideByFactor(copy.deepcopy(reportDataDict), unitData[1])
    if unitData == baseUnitData:
      reportDataDict = graphDataDict
    else:
      reportDataDict, dummyMaxValue = self._divideByFactor(reportDataDict, baseUnitData[1])
    return reportDataDict, graphDataDict, maxValue, unitData[0]

  def __checkPlotMetadata(self, metadata):
    """It check the plot metadata arguments

    :param dict metadata: contains the plot metadata
    """
    if self._extraArgs.get(self._EA_WIDTH):
      try:
        metadata[self._EA_WIDTH] = min(1600, max(200, int(self._extraArgs[self._EA_WIDTH])))
      except BaseException:
        pass
    if self._EA_HEIGHT in self._extraArgs and self._extraArgs[self._EA_HEIGHT]:
      try:
        metadata[self._EA_HEIGHT] = min(1600, max(200, int(self._extraArgs[self._EA_HEIGHT])))
      except BaseException:
        pass
    if self._extraArgs.get(self._EA_TITLE):
      metadata['title'] = self._extraArgs[self._EA_TITLE]

  def __checkThumbnailMetadata(self, metadata):
    """checks the plot thumbnail data

     :param dict metadata: contains the thumbnail data
     """
    if self._EA_THUMBNAIL in self._extraArgs and self._extraArgs[self._EA_THUMBNAIL]:
      thbMD = dict(metadata)
      thbMD['legend'] = False
      if self._EA_THB_HEIGHT in self._extraArgs:
        thbMD[self._EA_HEIGHT] = self._extraArgs[self._EA_THB_HEIGHT]
      else:
        thbMD[self._EA_HEIGHT] = 125
      if self._EA_THB_WIDTH in self._extraArgs:
        thbMD[self._EA_WIDTH] = self._extraArgs[self._EA_THB_WIDTH]
      else:
        thbMD[self._EA_WIDTH] = 200
      thbMD[self._EA_PADDING] = 20
      for key in ('title', 'ylabel', 'xlabel'):
        if key in thbMD:
          del thbMD[key]
      return thbMD
    return False

  def __plotData(self, filename, dataDict, metadata, funcToPlot):
    """It create the plot.

    :param str filename: the name of the file which contains the plot
    :param dict dataDict: data used to crate the plot
    :param dict metadata: plot metadata
    :param object funcToPlot: the method which create the plot using the appropriate method.
    """
    self.__checkPlotMetadata(metadata)
    if not dataDict:
      funcToPlot = generateNoDataPlot
    plotFileName = "%s.png" % filename

    finalResult = funcToPlot(fileName=plotFileName,
                             data=dataDict,
                             metadata=metadata)
    if not finalResult['OK']:
      return finalResult
    thbMD = self.__checkThumbnailMetadata(metadata)
    if not thbMD:
      return S_OK({'plot': True, 'thumbnail': False})
    thbFilename = "%s.thb.png" % filename
    retVal = funcToPlot(thbFilename, dataDict, thbMD)
    if not retVal['OK']:
      return retVal
    return S_OK({'plot': True, 'thumbnail': True})

  def _generateTimedStackedBarPlot(self, filename, dataDict, metadata):
    """
    it creates a bar plot
    """
    return self.__plotData(filename, dataDict, metadata, generateTimedStackedBarPlot)

  def _generateQualityPlot(self, filename, dataDict, metadata):
    """
    it create a quality plot
    """
    return self.__plotData(filename, dataDict, metadata, generateQualityPlot)

  def _generateCumulativePlot(self, filename, dataDict, metadata):
    """
    It creates a cumulative plot
    """
    return self.__plotData(filename, dataDict, metadata, generateCumulativePlot)

  def _generatePiePlot(self, filename, dataDict, metadata):
    """
    It creates a pie chart plot
    """
    return self.__plotData(filename, dataDict, metadata, generatePiePlot)

  def _generateStackedLinePlot(self, filename, dataDict, metadata):
    """
    It create a stacked lien plot
    """
    return self.__plotData(filename, dataDict, metadata, generateStackedLinePlot)

  def _fillWithZero(self, granularity, startEpoch, endEpoch, dataDict):
    """
    Fill with zeros missing buckets
      - dataDict = { 'key' : { time1 : value,  time2 : value... }, 'key2'.. }
    """
    startBucketEpoch = startEpoch - startEpoch % granularity
    for key in dataDict:
      currentDict = dataDict[key]
      for timeEpoch in range(int(startBucketEpoch), int(endEpoch), granularity):
        if timeEpoch not in currentDict:
          currentDict[timeEpoch] = 0
    return dataDict
