# -*- coding: utf-8 -*-
require "focuslight"
require "focuslight/config"
require "focuslight/logger"
require "focuslight/graph"

require "time"
require "tempfile"

require "rrd"

class Focuslight::RRD
  include Focuslight::Logger

  def initialize(args={})
    @datadir = Focuslight::Config.get(:datadir)
    # rrdcached
  end

  def rrd_create_options_long(dst)
    [
      '--step', '300',
      "DS:num:#{dst}:600:U:U",
      'RRA:AVERAGE:0.5:1:1440', # 5mins, 5days
      'RRA:AVERAGE:0.5:6:1008', # 30mins, 21days
      'RRA:AVERAGE:0.5:24:1344', # 2hours, 112days
      'RRA:AVERAGE:0.5:288:2500', # 24hours, 500days
      'RRA:MAX:0.5:1:1440', # 5mins, 5days
      'RRA:MAX:0.5:6:1008', # 30mins, 21days
      'RRA:MAX:0.5:24:1344', # 2hours, 112days
      'RRA:MAX:0.5:288:2500', # 24hours, 500days
    ]
  end

  def rrd_create_options_short(dst)
    [
      '--step', '60',
      "DS:num:#{dst}:120:U:U",
      'RRA:AVERAGE:0.5:1:4800', # 1min, 3days(80hours)
      'RRA:MAX:0.5:1:4800', # 1min, 3days(80hours)
    ]
  end

  def path(graph, target=:normal)
    dst = (graph.mode == 'derive' ? 'DERIVE' : 'GAUGE')
    filepath = nil
    rrdoptions = nil
    if target == :short
      filepath = File.join(@datadir, graph.md5 + '_s.rrd')
      rrdoptions = rrd_create_options_short(dst)
    else # :long
      filepath = File.join(@datadir, graph.md5 + '.rrd')
      rrdoptions = rrd_create_options_long(dst)
    end
    unless File.exists?(filepath)
      ret = RRD::Wrapper.create(filepath, *rrdoptions.map(&:to_s))
      unless ret
        # TODO: error logging / handling
        raise "RRDtool returns error to create #{filepath}, error: #{RRD::Wrapper.error}"
      end
    end
    filepath
  end

  def update(graph, target=:normal)
    file = path(graph, target)
    options = [
      file,
      '-t', 'num',
      '--', ['N', graph.number].join(':')
    ]
    ## TODO: rrdcached
    # if ( $self->{rrdcached} ) {
    #   # The caching daemon cannot be used together with templates (-t) yet.
    #   splice(@argv, 1, 2); # delete -t option
    #   unshift(@argv, '-d', $self->{rrdcached});
    # }
    ret = RRD::Wrapper.update(*options.map(&:to_s))
    unless ret
      raise "RRDtool returns error to update #{file}, error: #{RRD::Wrapper.error}"
    end
  end

  def calc_period(span, from, to)
    span ||= 'd'

    period_title = nil
    period = nil
    period_end = 'now'
    xgrid = nil

    case span
    when 'c', 'sc'
      from_time = Time.parse(from)             # from default: 8 days ago by '%Y/%m/%d %T'
      to_time = to ? Time.parse(to) : Time.now # to   default: now by '%Y/%m/%d %T'
      raise ArgumentError, "from(#{from}) is recent date than to(#{to})" if from_time > to_time
      period_title = "#{from} to #{to}"
      period = from_time.to_i
      period_end = to_time.to_i
      diff = to_time - from_time
      if diff < 3 * 60 * 60
        xgrid = 'MINUTE:10:MINUTE:20:MINUTE:10:0:%M'
      elsif diff < 4 * 24 * 60 * 60
        xgrid = 'HOUR:6:DAY:1:HOUR:6:0:%H'
      elsif diff < 14 * 24 * 60 * 60
        xgrid = 'DAY:1:DAY:1:DAY:2:86400:%m/%d'
      elsif diff < 45 * 24 * 60 * 60
        xgrid = 'DAY:1:WEEK:1:WEEK:1:0:%F'
      else
        xgrid = 'WEEK:1:MONTH:1:MONTH:1:2592000:%b'
      end
    when 'h', 'sh'
      period_title = (span == 'h' ? 'Hour (5min avg)' : 'Hour (1min avg)')
      period = -1 * 60 * 60 * 2
      xgrid = 'MINUTE:10:MINUTE:20:MINUTE:10:0:%M'
    when 'n', 'sn'
      period_title = (span == 'n' ? 'Half Day (5min avg)' : 'Half Day (1min avg)')
      period = -1 * 60 * 60 * 14
      xgrid = 'MINUTE:60:MINUTE:120:MINUTE:120:0:%H %M'
    when 'w'
      period_title = 'Week (30min avg)'
      period = -1 * 60 * 60 * 24 * 8
      xgrid = 'DAY:1:DAY:1:DAY:1:86400:%a'
    when 'm'
      period_title = 'Month (2hour avg)'
      period = -1 * 60 * 60 * 24 * 35
      xgrid = 'DAY:1:WEEK:1:WEEK:1:604800:Week %W'
    when 'y'
      period_title = 'Year (1day avg)'
      period = -1 * 60 * 60 * 24 * 400
      xgrid = 'WEEK:1:MONTH:1:MONTH:1:2592000:%b'
    when '3d', 's3d'
      period_title = (span == '3d' ? '3 Days (5min avg)' : '3 Days (1min avg)')
      period = -1 * 60 * 60 * 24 * 3
      xgrid = 'HOUR:6:DAY:1:HOUR:6:0:%H'
    when '8h', 's8h'
      period_title = (span == '8h' ? '8 Hours (5min avg)' : '8 Hours (1min avg)')
      period = -1 * 8 * 60 * 60
      xgrid = 'MINUTE:30:HOUR:1:HOUR:1:0:%H:%M'
    when '4h', 's4h'
      period_title = (span == '4h' ? '4 Hours (5min avg)' : '4 Hours (1min avg)')
      period = -1 * 4 * 60 * 60
      xgrid = 'MINUTE:30:HOUR:1:MINUTE:30:0:%H:%M'
    else # 'd' or 'sd' ?
      period_title = (span == 'sd' ? 'Day (1min avg)' : 'Day (5min avg)')
      period = -1 * 60 * 60 * 33 # 33 hours
      xgrid = 'HOUR:1:HOUR:2:HOUR:2:0:%H'
    end

    return period_title, period, period_end, xgrid
  end

  def graph(datas, args)
    datas = [datas] unless datas.is_a?(Array)
    span = args.fetch(:t, :d)
    from = args[:from]
    to = args[:to]
    width = args.fetch(:width, 390)
    height = args.fetch(:height, 110)

    period_title, period, period_end, xgrid = calc_period(span, from, to)

    tmpfile = Tempfile.new(["", ".png"]) # [basename_prefix, suffix]
    rrdoptions = [
      tmpfile.path,
      '-w', width,
      '-h', height,
      '-a', 'PNG',
      '-l', 0, #minimum
      '-u', 2, #maximum
      '-x', (args[:xgrid].empty? ? xgrid : args[:xgrid]),
      '-s', period,
      '-e', period_end,
      '--slope-mode',
      '--disable-rrdtool-tag',
      '--color', 'BACK#' + args[:background_color].to_s.upcase,
      '--color', 'CANVAS#' + args[:canvas_color].to_s.upcase,
      '--color', 'FONT#' + args[:font_color].to_s.upcase,
      '--color', 'FRAME#' + args[:frame_color].to_s.upcase,
      '--color', 'AXIS#' + args[:axis_color].to_s.upcase,
      '--color', 'SHADEA#' + args[:shadea_color].to_s.upcase,
      '--color', 'SHADEB#' + args[:shadeb_color].to_s.upcase,
      '--border', args[:border].to_s.upcase
    ]
    rrdoptions.push('-y', args[:ygrid]) unless args[:ygrid].empty?
    rrdoptions.push('-t', period_title.to_s.dup) unless args[:notitle]
    rrdoptions.push('-v', args[:vertical_label]) unless args[:vertical_label].empty?
    rrdoptions.push('--no-legend') unless args[:legend]
    rrdoptions.push('--only-graph') if args[:graphonly]
    rrdoptions.push('--logarithmic') if args[:logarithmic]

    rrdoptions.push('--font', "AXIS:8:")
    rrdoptions.push('--font', "LEGEND:8:")

    rrdoptions.push('-u', args[:upper_limit]) unless args[:upper_limit].empty?
    rrdoptions.push('-l', args[:lower_limit]) unless args[:lower_limit].empty?
    rrdoptions.push('-r') if args[:rigid]
    rrdoptions.push('--units-exponent', args[:units_exponent]) if args[:units_exponent]

    defs = []
    datas.each_with_index do |data, i|
      type  = data.c_type ? data.c_type : data.type
      gdata =  'num'
      llimit = data.llimit
      ulimit = data.ulimit
      stack = (data.stack && i > 0 ? ':STACK' : '')
      file = (span =~ /^s/ ? path(data, :short) : path(data, :long))
      unit = (data.unit || '').gsub('%', '%%')

      rrdoptions.push(
        'DEF:%s%dt=%s:%s:AVERAGE' % [gdata, i, file, gdata],
        'CDEF:%s%d=%s%dt,%s,%s,LIMIT,%d,%s' % [gdata, i, gdata, i, llimit, ulimit, data.adjustval, data.adjust],
        '%s:%s%d%s:%s %s' % [type, gdata, i, data.color, _escape(data.graph), stack],
        'GPRINT:%s%d:LAST:Cur\: %%4.1lf%%s%s' % [gdata, i, unit],
        'GPRINT:%s%d:AVERAGE:Avg\: %%4.1lf%%s%s' % [gdata, i, unit],
        'GPRINT:%s%d:MAX:Max\: %%4.1lf%%s%s' % [gdata, i, unit],
        'GPRINT:%s%d:MIN:Min\: %%4.1lf%%s%s\l' % [gdata, i, unit],
        'VDEF:%s%dcur=%s%d,LAST' % [gdata, i, gdata, i],
        'PRINT:%s%dcur:%%.8lf' % [gdata, i],
        'VDEF:%s%davg=%s%d,AVERAGE' % [gdata, i, gdata, i],
        'PRINT:%s%davg:%%.8lf' % [gdata, i],
        'VDEF:%s%dmax=%s%d,MAXIMUM' % [gdata, i, gdata, i],
        'PRINT:%s%dmax:%%.8lf' % [gdata, i],
        'VDEF:%s%dmin=%s%d,MINIMUM' % [gdata, i, gdata, i],
        'PRINT:%s%dmin:%%.8lf' % [gdata, i],
      )
      defs << ('%s%d' % [gdata, i])
    end

    if args[:sumup]
      sumup = [ defs.shift ]
      unit = datas.first.unit.gsub('%', '%%')
      defs.each do |d|
        sumup.push(d, '+')
      end
      rrdoptions.push(
        'CDEF:sumup=%s' % [ sumup.join(',') ],
        'LINE0:sumup#cccccc:total',
        'GPRINT:sumup:LAST:Cur\: %%4.1lf%%s%s' % [unit],
        'GPRINT:sumup:AVERAGE:Avg\: %%4.1lf%%s%s' % [unit],
        'GPRINT:sumup:MAX:Max\: %%4.1lf%%s%s' % [unit],
        'GPRINT:sumup:MIN:Min\: %%4.1lf%%s%s\l' % [unit],
        'VDEF:sumupcur=sumup,LAST',
        'PRINT:sumupcur:%.8lf',
        'VDEF:sumupavg=sumup,AVERAGE',
        'PRINT:sumupavg:%.8lf',
        'VDEF:sumupmax=sumup,MAXIMUM',
        'PRINT:sumupmax:%.8lf',
        'VDEF:sumupmin=sumup,MINIMUM',
        'PRINT:sumupmin:%.8lf',
      )
    end

    ret = RRD::Wrapper.graph(*rrdoptions.map(&:to_s))
    unless ret
      tmpfile.close!
      raise "RRDtool returns error to draw graph, error: #{RRD::Wrapper.error}"
    end

    # Cannot get last PRINT return value, set of [current,avg,max,min] of each data source
    # This makes 'summary' API not supported

    graph_img = IO.binread(tmpfile.path); # read as binary
    tmpfile.delete

    [
      "/var/folders/tl/xtb7dnc132nggd6hs83y58h40000gq/T/20140117-86285-1igjvvh.png",
      "-w", 390,
      "-h", 110,
      "-a", "PNG",
      "-l", 0,
      "-u", 2,
      "-x", "HOUR:1:HOUR:2:HOUR:2:0:%H",
      "-s", -118800,
      "-e", "now",
      "--slope-mode",
      "--disable-rrdtool-tag",
      "--color", "BACK#F3F3F3", "--color", "CANVAS#FFFFFF", "--color", "FONT#000000",
      "--color", "FRAME#000000", "--color", "AXIS#000000", "--color", "SHADEA#CFCFCF",
      "--color", "SHADEB#9E9E9E",
      "--border", "3",
      "-t", "Day (1min avg)",
      "--no-legend",
      "--font", "AXIS:8:",
      "--font", "LEGEND:8:",
      "DEF:num0t=./data/c4ca4238a0b923820dcc509a6f75849b.rrd:num:AVERAGE",
      "CDEF:num0=num0t,-1000000000.0,1.0e+15,LIMIT,1,*",
      "AREA:num0:one",
      "GPRINT:num0:LAST:Cur\\: %4.1lf%s",
      "GPRINT:num0:AVERAGE:Avg\\: %4.1lf%s",
      "GPRINT:num0:MAX:Max\\: %4.1lf%s",
      "GPRINT:num0:MIN:Min\\: %4.1lf%s\\l",
      "VDEF:num0cur=num0,LAST",
      "PRINT:num0cur:%.8lf",
      "VDEF:num0avg=num0,AVERAGE",
      "PRINT:num0avg:%.8lf",
      "VDEF:num0max=num0,MAXIMUM",
      "PRINT:num0max:%.8lf",
      "VDEF:num0min=num0,MINIMUM",
      "PRINT:num0min:%.8lf"
    ]

    graph_img
  end

  def export(datas, args)
    datas = [datas] unless datas.is_a?(Array)
    span = args.fetch(:t, 'd')
    from = args[:from]
    to = args[:to]
    width = args.fetch(:width, 390)
    cf = args[:cf]

    period_title, period, period_end, xgrid = calc_period(span, from, to)

    rrdoptions = [
      '-m', width,
      '-s', period,
      '-e', period_end
    ]

    rrdoptions.push('--step', args[:step]) if args[:step]

    defs = []
    datas.each_with_index do |data, i|
      type  = data.c_type ? data.c_type : data.type
      gdata =  'num'
      llimit = data.llimit
      ulimit = data.ulimit
      stack = (data.stack && i > 0 ? ':STACK' : '')
      file = (span =~ /^s/ ? path(data, :short) : path(data, :long))

      rrdoptions.push(
        'DEF:%s%dt=%s:%s:%s' % [gdata, i, file, gdata, cf],
        'CDEF:%s%d=%s%dt,%s,%s,LIMIT,%d,%s' % [gdata, i, gdata, i, llimit, ulimit, data.adjustval, data.adjust],
        'XPORT:%s%d:%s' % [gdata, i, _escape(data.graph)]
      )
      defs << ('%s%d' % [gdata, i])
    end

    if args[:sumup]
      sumup = [ defs.shift ]
      defs.each do |d|
        sumup.push(d, '+')
      end
      rrdoptions.push(
        'CDEF:sumup=%s' % [sumup.join(',')],
        'XPORT:sumup:total'
      )
    end

    ret = RRD::Wrapper.xport(*rrdoptions.map(&:to_s))
    unless ret
      raise "RRDtool returns error to xport, error: #{RRD::Wrapper.error}"
    end
    ### copied from RRD::Wrapper spec
    # values = RRD::Wrapper.xport("--start", "1266933600", "--end", "1266944400", "DEF:xx=#{RRD_FILE}:cpu0:AVERAGE", "XPORT:xx:Legend 0")
    # values[0..-2].should == [["time", "Legend 0"], [1266933600, 0.0008], [1266937200, 0.0008], [1266940800, 0.0008]]
    cols_row = ret.shift

    column_names = cols_row[1..-1] # cols_row[0] == 'time'
    columns = column_names.length
    start_timestamp = ret.first.first
    end_timestamp = ret.last.first
    step = ret[1].first - ret[0].first
    rows = []
    ret.each do |values|
      # GrowthForecast compatibility NaN to nil
      rows << values[1..-1].map {|v| v.nan? ? nil : v}
    end

    {
      'start_timestamp' => start_timestamp,
      'end_timestamp' => end_timestamp,
      'step' => step,
      'columns' => columns,
      'column_names' => column_names,
      'rows' => rows,
    }
  end

  def remove(graph)
    [File.join(@datadir, graph.md5 + '.rrd'), File.join(@datadir, graph.md5 + '_s.rrd')].each do |file|
      begin
        File.delete(file)
      rescue => e
        # ignore NOSUCHFILE or others
      end
    end
  end

  def _escape(str)
    str.gsub(':', '\:')
  end
end
