""" GraphUtilities is a a collection of utility functions and classes used
    in the DIRAC Graphs package.

    The DIRAC Graphs package is derived from the GraphTool plotting package of the
    CMS/Phedex Project by ... <to be added>
"""
from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

__RCSID__ = "$Id$"


import six
import os
import time
import datetime
import calendar
import math
import pytz
import numpy

from matplotlib.pyplot import rcParams
from matplotlib.ticker import ScalarFormatter
from matplotlib.dates import AutoDateLocator, AutoDateFormatter, DateFormatter, RRuleLocator, \
    rrulewrapper, HOURLY, MINUTELY, SECONDLY, YEARLY, MONTHLY, DAILY
from dateutil.relativedelta import relativedelta

# This is a hack to workaround the use of float(ScalarFormatter.__call__(...))
rcParams["axes.unicode_minus"] = False


def evalPrefs(*args, **kw):
  """ Interpret arguments as preferencies dictionaries or key-value pairs. The overriding order
      is right most - most important one. Returns a single dictionary of preferencies
  """

  prefs = {}
  for pDict in list(args) + [kw]:
    if isinstance(pDict, dict):
      for key in pDict:
        if key == "metadata":
          for mkey in pDict[key]:
            prefs[mkey] = pDict[key][mkey]
        else:
          prefs[key] = pDict[key]

  return prefs


def pixelToPoint(size, dpi):
  """ Convert size expressed in pixels into points for a given dpi resolution
  """

  return float(size) * 100. / float(dpi)


datestrings = ['%x %X', '%x', '%Y-%m-%d %H:%M:%S']


def convert_to_datetime(dstring):
  orig_string = str(dstring)
  try:
    if isinstance(dstring, datetime.datetime):
      results = dstring
    else:
      results = eval(str(dstring), {'__builtins__': None, 'time': time, 'math': math}, {})
    if isinstance(results, six.integer_types + (float,)):
      results = datetime.datetime.fromtimestamp(int(results))
    elif isinstance(results, datetime.datetime):
      pass
    else:
      raise ValueError("Unknown datetime type!")
  except Exception as e:
    t = None
    for dateformat in datestrings:
      try:
        t = time.strptime(dstring, dateformat)
        timestamp = calendar.timegm(t)  # -time.timezone
        results = datetime.datetime.fromtimestamp(timestamp)
        break
      except BaseException:
        pass
    if t is None:
      try:
        dstring = dstring.split('.', 1)[0]
        t = time.strptime(dstring, dateformat)
        timestamp = time.mktime(t)  # -time.timezone
        results = datetime.datetime.fromtimestamp(timestamp)
      except BaseException:
        raise ValueError("Unable to create time from string!\nExpecting "
                         "format of: '12/06/06 12:54:67'\nRecieved:%s" % orig_string)
  return results


def to_timestamp(val):

  try:
    v = float(val)
    if v > 1000000000 and v < 1900000000:
      return v
  except BaseException:
    pass

  val = convert_to_datetime(val)
  # return calendar.timegm( val.timetuple() )
  return time.mktime(val.timetuple())


# If the graph has more than `hour_switch` minutes, we print
# out hours in the subtitle.
hour_switch = 7

# If the graph has more than `day_switch` hours, we print
# out days in the subtitle.
day_switch = 7

# If the graph has more than `week_switch` days, we print
# out the weeks in the subtitle.
week_switch = 7


def add_time_to_title(begin, end, metadata={}):
  """ Given a title and two times, adds the time info to the title.
      Example results::

         "Number of Attempted Transfers
         (24 Hours from 4:45 12-14-2006 to 5:56 12-15-2006)"

      There are two important pieces to the subtitle we add - the duration
      (i.e., '48 Hours') and the time interval (i.e., 11:00 07-02-2007 to
      11:00 07-04-2007).

      We attempt to make the duration match the size of the span (for a bar
      graph, this would be the width of the individual bar) in order for it
      to make the most sense.  The formatting of the time interval is based
      upon how much real time there is from the beginning to the end.

      We made the distinction because some would want to show graphs
      representing 168 Hours, but needed the format to show the date as
      well as the time.
  """
  if 'span' in metadata:
    interval = metadata['span']
  else:
    interval = time_interval(begin, end)
  formatting_interval = time_interval(begin, end)
  if formatting_interval == 600:
    format_str = '%H:%M:%S'
  elif formatting_interval == 3600:
    format_str = '%Y-%m-%d %H:%M'
  elif formatting_interval == 86400:
    format_str = '%Y-%m-%d'
  elif formatting_interval == 86400 * 7:
    format_str = 'Week %U of %Y'

  if interval < 600:
    format_name = 'Seconds'
    time_slice = 1
  elif interval < 3600 and interval >= 600:
    format_name = 'Minutes'
    time_slice = 60
  elif interval >= 3600 and interval < 86400:
    format_name = 'Hours'
    time_slice = 3600
  elif interval >= 86400 and interval < 86400 * 7:
    format_name = 'Days'
    time_slice = 86400
  elif interval >= 86400 * 7:
    format_name = 'Weeks'
    time_slice = 86400 * 7
  else:
    format_str = '%x %X'
    format_name = 'Seconds'
    time_slice = 1

  begin_tuple = time.localtime(begin)
  end_tuple = time.localtime(end)
  added_title = '%i %s from ' % (int((end - begin) / time_slice), format_name)
  added_title += time.strftime('%s to' % format_str, begin_tuple)
  if time_slice < 86400:
    add_utc = ' UTC'
  else:
    add_utc = ''
  added_title += time.strftime(' %s%s' % (format_str, add_utc), end_tuple)
  return added_title


def time_interval(begin, end):
  """
  Determine the appropriate time interval based upon the length of
  time as indicated by the `starttime` and `endtime` keywords.
  """

  if end - begin < 600 * hour_switch:
    return 600
  if end - begin < 86400 * day_switch:
    return 3600
  elif end - begin < 86400 * 7 * week_switch:
    return 86400
  else:
    return 86400 * 7


def comma_format(x_orig):
  x = float(x_orig)
  if x >= 1000:
    after_comma = x % 1000
    before_comma = int(int(x) / 1000)
    return '%s,%03g' % (comma_format(before_comma), after_comma)
  else:
    return str(x_orig)


class PrettyScalarFormatter(ScalarFormatter):
  def __init__(self, *args, **kwargs):
    super(PrettyScalarFormatter, self).__init__(*args, **kwargs)
    self.set_powerlimits([-7, 9])
    self._useLocale = True

  def __call__(self, x, pos=None):
    val = super(PrettyScalarFormatter, self).__call__(x, pos=pos)
    if self.offset:
      return val
    else:
      return comma_format(val)


class PrettyDateFormatter(AutoDateFormatter):
  """ This class provides a formatter which conforms to the
      desired date formats for the Phedex system.
  """

  def __init__(self, locator):
    """Format dates according to the Phedex system"""
    tz = pytz.timezone('UTC')
    AutoDateFormatter.__init__(self, locator, tz=tz)

  def __call__(self, x, pos=0):
    scale = float(self._locator._get_unit())
    if scale == 365.0:
      self._formatter = DateFormatter("%Y", self._tz)
    elif scale == 30.0:
      self._formatter = DateFormatter("%b %Y", self._tz)
    elif (scale >= 1.0) and (scale <= 7.0):
      self._formatter = DateFormatter("%Y-%m-%d", self._tz)
    elif scale == (1.0 / 24.0):
      self._formatter = DateFormatter("%H:%M", self._tz)
    elif scale == (1.0 / (24 * 60)):
      self._formatter = DateFormatter("%H:%M", self._tz)
    elif scale == (1.0 / (24 * 3600)):
      self._formatter = DateFormatter("%H:%M:%S", self._tz)
    else:
      self._formatter = DateFormatter("%b %d %Y %H:%M:%S", self._tz)

    return self._formatter(x, pos)


class PrettyDateLocator(AutoDateLocator):

  def get_locator(self, dmin, dmax):
    'pick the best locator based on a distance'

    delta = relativedelta(dmax, dmin)
    numYears = (delta.years * 1.0)
    numMonths = (numYears * 12.0) + delta.months
    numDays = (numMonths * 31.0) + delta.days
    numHours = (numDays * 24.0) + delta.hours
    numMinutes = (numHours * 60.0) + delta.minutes
    numSeconds = (numMinutes * 60.0) + delta.seconds

    numticks = 5

    # self._freq = YEARLY
    interval = 1
    bymonth = 1
    bymonthday = 1
    byhour = 0
    byminute = 0
    bysecond = 0

    if numYears >= numticks:
      self._freq = YEARLY
    elif numMonths >= numticks:
      self._freq = MONTHLY
      bymonth = list(range(1, 13))
      if (0 <= numMonths) and (numMonths <= 14):
        interval = 1      # show every month
      elif (15 <= numMonths) and (numMonths <= 29):
        interval = 3      # show every 3 months
      elif (30 <= numMonths) and (numMonths <= 44):
        interval = 4      # show every 4 months
      else:   # 45 <= numMonths <= 59
        interval = 6      # show every 6 months
    elif numDays >= numticks:
      self._freq = DAILY
      bymonth = None
      bymonthday = list(range(1, 32))
      if (0 <= numDays) and (numDays <= 9):
        interval = 1      # show every day
      elif (10 <= numDays) and (numDays <= 19):
        interval = 2      # show every 2 days
      elif (20 <= numDays) and (numDays <= 35):
        interval = 3      # show every 3 days
      elif (36 <= numDays) and (numDays <= 80):
        interval = 7      # show every 1 week
      else:   # 100 <= numDays <= ~150
        interval = 14     # show every 2 weeks
    elif numHours >= numticks:
      self._freq = HOURLY
      bymonth = None
      bymonthday = None
      byhour = list(range(0, 24))      # show every hour
      if (0 <= numHours) and (numHours <= 14):
        interval = 1      # show every hour
      elif (15 <= numHours) and (numHours <= 30):
        interval = 2      # show every 2 hours
      elif (30 <= numHours) and (numHours <= 45):
        interval = 3      # show every 3 hours
      elif (45 <= numHours) and (numHours <= 68):
        interval = 4      # show every 4 hours
      elif (68 <= numHours) and (numHours <= 90):
        interval = 6      # show every 6 hours
      else:   # 90 <= numHours <= 120
        interval = 12     # show every 12 hours
    elif numMinutes >= numticks:
      self._freq = MINUTELY
      bymonth = None
      bymonthday = None
      byhour = None
      byminute = list(range(0, 60))
      if numMinutes > (10.0 * numticks):
        interval = 10
      # end if
    elif numSeconds >= numticks:
      self._freq = SECONDLY
      bymonth = None
      bymonthday = None
      byhour = None
      byminute = None
      bysecond = list(range(0, 60))
      if numSeconds > (10.0 * numticks):
        interval = 10
      # end if
    else:
      # do what?
      #   microseconds as floats, but floats from what reference point?
      pass

    rrule = rrulewrapper(self._freq, interval=interval,
                         dtstart=dmin, until=dmax,
                         bymonth=bymonth, bymonthday=bymonthday,
                         byhour=byhour, byminute=byminute,
                         bysecond=bysecond)

    locator = RRuleLocator(rrule, self.tz)
    locator.set_axis(self.axis)

    locator.set_view_interval(*self.axis.get_view_interval())
    locator.set_data_interval(*self.axis.get_data_interval())
    return locator


def pretty_float(num):

  if num > 1000:
    return comma_format(int(num))

  try:
    floats = int(max(2 - max(numpy.floor(numpy.log(abs(num) + 1e-3) / numpy.log(10.)), 0), 0))
  except BaseException:
    floats = 2
  format = "%." + str(floats) + "f"
  if isinstance(num, tuple):
    return format % float(num[0])
  else:
    try:
      retval = format % float(num)
    except BaseException:
      raise Exception("Unable to convert %s into a float." % (str(num)))
    return retval


def statistics(results, span=None, is_timestamp=False):
  results = dict(results)
  if span is not None:
    parsed_data = {}
    min_key = min(results)
    max_key = max(results)
    for i in range(min_key, max_key + span, span):
      if i in results:
        parsed_data[i] = results[i]
        del results[i]
      else:
        parsed_data[i] = 0.0
    if len(results) > 0:
      raise Exception("Unable to use all the values for the statistics")
  else:
    parsed_data = results
  values = list(parsed_data.values())
  data_min = min(values)
  data_max = max(values)
  data_avg = numpy.average(values)
  if is_timestamp:
    current_time = max(parsed_data)
    data_current = parsed_data[current_time]
    return data_min, data_max, data_avg, data_current
  else:
    return data_min, data_max, data_avg


def makeDataFromCSV(csv):
  """ Generate plot data dictionary from a csv file or string
  """

  if os.path.exists(csv):
    with open(csv, 'r') as fdata:
      flines = fdata.readlines()
  else:
    flines = csv.split('\n')

  graph_data = {}
  labels = flines[0].strip().split(',')
  if len(labels) == 2:
    # simple plot data
    for line in flines:
      line = line.strip()
      if line[0] != '#':
        key, value = line.split(',')
        graph_data[key] = value

  elif len(flines) == 2:
    values = flines[1].strip().split(',')
    for key, value in zip(labels, values):
      graph_data[key] = value

  elif len(labels) > 2:
    # stacked graph data
    del labels[0]
    del flines[0]
    for label in labels:
      plot_data = {}
      index = labels.index(label) + 1
      for line in flines:
        values = line.strip().split(',')
        value = values[index].strip()
        # if value:
        plot_data[values[0]] = values[index]
        # else:
        # plot_data[values[0]] = '0.'
        # pass
      graph_data[label] = dict(plot_data)

  return graph_data


def darkenColor(color, factor=2):

  c1 = int(color[1:3], 16)
  c2 = int(color[3:5], 16)
  c3 = int(color[5:7], 16)

  c1 /= factor
  c2 /= factor
  c3 /= factor

  result = '#' + (str(hex(c1)).replace('0x', '').zfill(2) +
                  str(hex(c2)).replace('0x', '').zfill(2) +
                  str(hex(c3)).replace('0x', '').zfill(2))
  return result
