class Morris.Grid extends Morris.EventEmitter
  # A generic pair of axes for line/area/bar charts.
  #
  # Draws grid lines and axis labels.
  #
  constructor: (options) ->
    # find the container to draw the graph in
    if typeof options.element is 'string'
      @el = $ document.getElementById(options.element)
    else
      @el = $ options.element
    if not @el? or @el.length == 0
      throw new Error("Graph container element not found")

    if @el.css('position') == 'static'
      @el.css('position', 'relative')

    @options = $.extend {}, @gridDefaults, (@defaults || {}), options

    # backwards compatibility for units -> postUnits
    if typeof @options.units is 'string'
      @options.postUnits = options.units

    # the raphael drawing instance
    @raphael = new Raphael(@el[0])

    # some redraw stuff
    @elementWidth = null
    @elementHeight = null
    @dirty = false

    # more stuff
    @init() if @init

    # load data
    @setData @options.data

    # hover
    @el.bind 'mousemove', (evt) =>
      offset = @el.offset()
      @fire 'hovermove', evt.pageX - offset.left, evt.pageY - offset.top

    @el.bind 'mouseout', (evt) =>
      @fire 'hoverout'

    @el.bind 'touchstart touchmove touchend', (evt) =>
      touch = evt.originalEvent.touches[0] or evt.originalEvent.changedTouches[0]
      offset = @el.offset()
      @fire 'hover', touch.pageX - offset.left, touch.pageY - offset.top
      touch

    @el.bind 'click', (evt) =>
      offset = @el.offset()
      @fire 'gridclick', evt.pageX - offset.left, evt.pageY - offset.top

    @postInit() if @postInit

  # Default options
  #
  gridDefaults:
    dateFormat: null
    axes: true
    grid: true
    gridLineColor: '#aaa'
    gridStrokeWidth: 0.5
    gridTextColor: '#888'
    gridTextSize: 12
    gridTextFamily: 'sans-serif'
    gridTextWeight: 'normal'
    hideHover: false
    yLabelFormat: null
    xLabelAngle: 0
    numLines: 5
    padding: 25
    parseTime: true
    postUnits: ''
    preUnits: ''
    ymax: 'auto'
    ymin: 'auto 0'
    goals: []
    goalStrokeWidth: 1.0
    goalLineColors: [
      '#666633'
      '#999966'
      '#cc6666'
      '#663333'
    ]
    events: []
    eventStrokeWidth: 1.0
    eventLineColors: [
      '#005a04'
      '#ccffbb'
      '#3a5f0b'
      '#005502'
    ]

  # Update the data series and redraw the chart.
  #
  setData: (data, redraw = true) ->
    @options.data = data

    if !data? or data.length == 0
      @data = []
      @raphael.clear()
      @hover.hide() if @hover?
      return

    ymax = if @cumulative then 0 else null
    ymin = if @cumulative then 0 else null

    if @options.goals.length > 0
      minGoal = Math.min.apply(null, @options.goals)
      maxGoal = Math.max.apply(null, @options.goals)
      ymin = if ymin? then Math.min(ymin, minGoal) else minGoal
      ymax = if ymax? then Math.max(ymax, maxGoal) else maxGoal

    @data = for row, index in data
      ret = {}

      ret.label = row[@options.xkey]
      if @options.parseTime
        ret.x = Morris.parseDate(ret.label)
        if @options.dateFormat
          ret.label = @options.dateFormat ret.x
        else if typeof ret.label is 'number'
          ret.label = new Date(ret.label).toString()
      else
        ret.x = index
        if @options.xLabelFormat
          ret.label = @options.xLabelFormat ret
      total = 0
      ret.y = for ykey, idx in @options.ykeys
        yval = row[ykey]
        yval = parseFloat(yval) if typeof yval is 'string'
        yval = null if yval? and typeof yval isnt 'number'
        if yval?
          if @cumulative
            total += yval
          else
            if ymax?
              ymax = Math.max(yval, ymax)
              ymin = Math.min(yval, ymin)
            else
              ymax = ymin = yval
        if @cumulative and total?
          ymax = Math.max(total, ymax)
          ymin = Math.min(total, ymin)
        yval
      ret

    if @options.parseTime
      @data = @data.sort (a, b) -> (a.x > b.x) - (b.x > a.x)

    # calculate horizontal range of the graph
    @xmin = @data[0].x
    @xmax = @data[@data.length - 1].x

    @events = []
    if @options.parseTime and @options.events.length > 0
      @events = (Morris.parseDate(e) for e in @options.events)
      @xmax = Math.max(@xmax, Math.max.apply(null, @events))
      @xmin = Math.min(@xmin, Math.min.apply(null, @events))

    if @xmin is @xmax
      @xmin -= 1
      @xmax += 1

    @ymin = @yboundary('min', ymin)
    @ymax = @yboundary('max', ymax)

    if @ymin is @ymax
      @ymin -= 1 if ymin
      @ymax += 1

    if @options.axes is true or @options.grid is true
      if (@options.ymax == @gridDefaults.ymax and
          @options.ymin == @gridDefaults.ymin)
        # calculate 'magic' grid placement
        @grid = @autoGridLines(@ymin, @ymax, @options.numLines)
        @ymin = Math.min(@ymin, @grid[0])
        @ymax = Math.max(@ymax, @grid[@grid.length - 1])
      else
        step = (@ymax - @ymin) / (@options.numLines - 1)
        @grid = (y for y in [@ymin..@ymax] by step)

    @dirty = true
    @redraw() if redraw

  yboundary: (boundaryType, currentValue) ->
    boundaryOption = @options["y#{boundaryType}"]
    if typeof boundaryOption is 'string'
      if boundaryOption[0..3] is 'auto'
        if boundaryOption.length > 5
          suggestedValue = parseInt(boundaryOption[5..], 10)
          return suggestedValue unless currentValue?
          Math[boundaryType](currentValue, suggestedValue)
        else
          if currentValue? then currentValue else 0
      else
        parseInt(boundaryOption, 10)
    else
      boundaryOption

  autoGridLines: (ymin, ymax, nlines) ->
    span = ymax - ymin
    ymag = Math.floor(Math.log(span) / Math.log(10))
    unit = Math.pow(10, ymag)

    # calculate initial grid min and max values
    gmin = Math.floor(ymin / unit) * unit
    gmax = Math.ceil(ymax / unit) * unit
    step = (gmax - gmin) / (nlines - 1)
    if unit == 1 and step > 1 and Math.ceil(step) != step
      step = Math.ceil(step)
      gmax = gmin + step * (nlines - 1)

    # ensure zero is plotted where the range includes zero
    if gmin < 0 and gmax > 0
      gmin = Math.floor(ymin / step) * step
      gmax = Math.ceil(ymax / step) * step

    # special case for decimal numbers
    if step < 1
      smag = Math.floor(Math.log(step) / Math.log(10))
      grid = for y in [gmin..gmax] by step
        parseFloat(y.toFixed(1 - smag))
    else
      grid = (y for y in [gmin..gmax] by step)
    grid

  _calc: ->
    w = @el.width()
    h = @el.height()

    if @elementWidth != w or @elementHeight != h or @dirty
      @elementWidth = w
      @elementHeight = h
      @dirty = false
      # recalculate grid dimensions
      @left = @options.padding
      @right = @elementWidth - @options.padding
      @top = @options.padding
      @bottom = @elementHeight - @options.padding
      if @options.axes
        yLabelWidths = for gridLine in @grid
          @measureText(@yAxisFormat(gridLine)).width
        @left += Math.max(yLabelWidths...)
        bottomOffsets = for i in [0...@data.length]
          @measureText(@data[i].text, -@options.xLabelAngle).height
        @bottom -= Math.max(bottomOffsets...)
      @width = Math.max(1, @right - @left)
      @height = Math.max(1, @bottom - @top)
      @dx = @width / (@xmax - @xmin)
      @dy = @height / (@ymax - @ymin)
      @calc() if @calc

  # Quick translation helpers
  #
  transY: (y) -> @bottom - (y - @ymin) * @dy
  transX: (x) ->
    if @data.length == 1
      (@left + @right) / 2
    else
      @left + (x - @xmin) * @dx

  # Draw it!
  #
  # If you need to re-size your charts, call this method after changing the
  # size of the container element.
  redraw: ->
    @raphael.clear()
    @_calc()
    @drawGrid()
    @drawGoals()
    @drawEvents()
    @draw() if @draw

  # @private
  #
  measureText: (text, angle = 0) ->
    tt = @raphael.text(100, 100, text)
      .attr('font-size', @options.gridTextSize)
      .attr('font-family', @options.gridTextFamily)
      .attr('font-weight', @options.gridTextWeight)
      .rotate(angle)
    ret = tt.getBBox()
    tt.remove()
    ret

  # @private
  #
  yAxisFormat: (label) -> @yLabelFormat(label)

  # @private
  #
  yLabelFormat: (label) ->
    if typeof @options.yLabelFormat is 'function'
      @options.yLabelFormat(label)
    else
      "#{@options.preUnits}#{Morris.commas(label)}#{@options.postUnits}"

  updateHover: (x, y) ->
    hit = @hitTest(x, y)
    if hit?
      @hover.update(hit...)

  # draw y axis labels, horizontal lines
  #
  drawGrid: ->
    return if @options.grid is false and @options.axes is false
    for lineY in @grid
      y = @transY(lineY)
      if @options.axes
        @drawYAxisLabel(@left - @options.padding / 2, y, @yAxisFormat(lineY))
      if @options.grid
        @drawGridLine("M#{@left},#{y}H#{@left + @width}")

  # draw goals horizontal lines
  #
  drawGoals: ->
    for goal, i in @options.goals
      color = @options.goalLineColors[i % @options.goalLineColors.length]
      @drawGoal(goal, color)

  # draw events vertical lines
  drawEvents: ->
    for event, i in @events
      color = @options.eventLineColors[i % @options.eventLineColors.length]
      @drawEvent(event, color)

  drawGoal: (goal, color) ->
    @raphael.path("M#{@left},#{@transY(goal)}H#{@right}")
      .attr('stroke', color)
      .attr('stroke-width', @options.goalStrokeWidth)

  drawEvent: (event, color) ->
    @raphael.path("M#{@transX(event)},#{@bottom}V#{@top}")
      .attr('stroke', color)
      .attr('stroke-width', @options.eventStrokeWidth)

  drawYAxisLabel: (xPos, yPos, text) ->
    @raphael.text(xPos, yPos, text)
      .attr('font-size', @options.gridTextSize)
      .attr('font-family', @options.gridTextFamily)
      .attr('font-weight', @options.gridTextWeight)
      .attr('fill', @options.gridTextColor)
      .attr('text-anchor', 'end')

  drawGridLine: (path) ->
    @raphael.path(path)
      .attr('stroke', @options.gridLineColor)
      .attr('stroke-width', @options.gridStrokeWidth)

# Parse a date into a javascript timestamp
#
#
Morris.parseDate = (date) ->
  if typeof date is 'number'
    return date
  m = date.match /^(\d+) Q(\d)$/
  n = date.match /^(\d+)-(\d+)$/
  o = date.match /^(\d+)-(\d+)-(\d+)$/
  p = date.match /^(\d+) W(\d+)$/
  q = date.match /^(\d+)-(\d+)-(\d+)[ T](\d+):(\d+)(Z|([+-])(\d\d):?(\d\d))?$/
  r = date.match /^(\d+)-(\d+)-(\d+)[ T](\d+):(\d+):(\d+(\.\d+)?)(Z|([+-])(\d\d):?(\d\d))?$/
  if m
    new Date(
      parseInt(m[1], 10),
      parseInt(m[2], 10) * 3 - 1,
      1).getTime()
  else if n
    new Date(
      parseInt(n[1], 10),
      parseInt(n[2], 10) - 1,
      1).getTime()
  else if o
    new Date(
      parseInt(o[1], 10),
      parseInt(o[2], 10) - 1,
      parseInt(o[3], 10)).getTime()
  else if p
    # calculate number of weeks in year given
    ret = new Date(parseInt(p[1], 10), 0, 1);
    # first thursday in year (ISO 8601 standard)
    if ret.getDay() isnt 4
      ret.setMonth(0, 1 + ((4 - ret.getDay()) + 7) % 7);
    # add weeks
    ret.getTime() + parseInt(p[2], 10) * 604800000
  else if q
    if not q[6]
      # no timezone info, use local
      new Date(
        parseInt(q[1], 10),
        parseInt(q[2], 10) - 1,
        parseInt(q[3], 10),
        parseInt(q[4], 10),
        parseInt(q[5], 10)).getTime()
    else
      # timezone info supplied, use UTC
      offsetmins = 0
      if q[6] != 'Z'
        offsetmins = parseInt(q[8], 10) * 60 + parseInt(q[9], 10)
        offsetmins = 0 - offsetmins if q[7] == '+'
      Date.UTC(
        parseInt(q[1], 10),
        parseInt(q[2], 10) - 1,
        parseInt(q[3], 10),
        parseInt(q[4], 10),
        parseInt(q[5], 10) + offsetmins)
  else if r
    secs = parseFloat(r[6])
    isecs = Math.floor(secs)
    msecs = Math.round((secs - isecs) * 1000)
    if not r[8]
      # no timezone info, use local
      new Date(
        parseInt(r[1], 10),
        parseInt(r[2], 10) - 1,
        parseInt(r[3], 10),
        parseInt(r[4], 10),
        parseInt(r[5], 10),
        isecs,
        msecs).getTime()
    else
      # timezone info supplied, use UTC
      offsetmins = 0
      if r[8] != 'Z'
        offsetmins = parseInt(r[10], 10) * 60 + parseInt(r[11], 10)
        offsetmins = 0 - offsetmins if r[9] == '+'
      Date.UTC(
        parseInt(r[1], 10),
        parseInt(r[2], 10) - 1,
        parseInt(r[3], 10),
        parseInt(r[4], 10),
        parseInt(r[5], 10) + offsetmins,
        isecs,
        msecs)
  else
    new Date(parseInt(date, 10), 0, 1).getTime()

