VALUE_HTML = '<span class="odometer-value"></span>'
RIBBON_HTML = '<span class="odometer-ribbon"><span class="odometer-ribbon-inner">' + VALUE_HTML + '</span></span>'
DIGIT_HTML = '<span class="odometer-digit"><span class="odometer-digit-spacer">8</span><span class="odometer-digit-inner">' + RIBBON_HTML + '</span></span>'
FORMAT_MARK_HTML = '<span class="odometer-formatting-mark"></span>'

# The bit within the parenthesis will be repeated, so (,ddd) becomes 123,456,789....
#
# If your locale uses spaces to seperate digits, you could consider using a
# Narrow No-Break Space ( ), as it's a bit more correct.
#
# Numbers will be rounded to the number of digits after the radix seperator.
#
# When values are set using `.update` or the `.innerHTML`-type attributes,
# strings are assumed to already be in the locale's format.
#
# This is just the default, it can also be set as options.format.
DIGIT_FORMAT = '(,ddd).dd'

FORMAT_PARSER = /^\(?([^)]*)\)?(?:(.)(d+))?$/

# What is our target framerate?
FRAMERATE = 30

# How long will the animation last?
DURATION = 2000

# What is the fastest we should update values when we are
# counting up (not using the wheel animation).
COUNT_FRAMERATE = 20

# What is the minimum number of frames for each value on the wheel?
# We won't render more values than could be reasonably seen
FRAMES_PER_VALUE = 2

# If more than one digit is hitting the frame limit, they would all get
# capped at that limit and appear to be moving at the same rate.  This
# factor adds a boost to subsequent digits to make them appear faster.
DIGIT_SPEEDBOOST = .5

MS_PER_FRAME = 1000 / FRAMERATE
COUNT_MS_PER_FRAME = 1000 / COUNT_FRAMERATE

TRANSITION_END_EVENTS = 'transitionend webkitTransitionEnd oTransitionEnd otransitionend MSTransitionEnd'

transitionCheckStyles = document.createElement('div').style
TRANSITION_SUPPORT = transitionCheckStyles.transition? or transitionCheckStyles.webkitTransition? or
                     transitionCheckStyles.mozTransition? or transitionCheckStyles.oTransition?

requestAnimationFrame = window.requestAnimationFrame or window.mozRequestAnimationFrame or
                        window.webkitRequestAnimationFrame or window.msRequestAnimationFrame

MutationObserver = window.MutationObserver or window.WebKitMutationObserver or window.MozMutationObserver

createFromHTML = (html) ->
  el = document.createElement('div')
  el.innerHTML = html
  el.children[0]

removeClass = (el, name) ->
  el.className = el.className.replace new RegExp("(^| )#{ name.split(' ').join('|') }( |$)", 'gi'), ' '

addClass = (el, name) ->
  removeClass el, name
  el.className += " #{ name }"

trigger = (el, name) ->
  # Custom DOM events are not supported in IE8
  if document.createEvent?
    evt = document.createEvent('HTMLEvents')
    evt.initEvent(name, true, true)
    el.dispatchEvent(evt)

now = ->
  window.performance?.now?() ? +new Date

round = (val, precision=0) ->
  return Math.round(val) unless precision

  val *= Math.pow(10, precision)
  val += 0.5
  val = Math.floor(val)
  val /= Math.pow(10, precision)

truncate = (val) ->
  # | 0 fails on numbers greater than 2^32
  if val < 0
    Math.ceil(val)
  else
    Math.floor(val)

fractionalPart = (val) ->
  val - round(val)

_jQueryWrapped = false
do wrapJQuery = ->
  return if _jQueryWrapped

  if window.jQuery?
    _jQueryWrapped = true
    # We need to wrap jQuery's .html and .text because they don't always
    # call .innerHTML/.innerText
    for property in ['html', 'text']
      do (property) ->
        old = window.jQuery.fn[property]
        window.jQuery.fn[property] = (val) ->
          if not val? or not this[0]?.odometer?
            return old.apply this, arguments

          this[0].odometer.update val

# In case jQuery is brought in after this file
setTimeout wrapJQuery, 0

class Odometer
  constructor: (@options) ->
    @el = @options.el
    return @el.odometer if @el.odometer?

    @el.odometer = @

    for k, v of Odometer.options
      if not @options[k]?
        @options[k] = v

    @options.duration ?= DURATION
    @MAX_VALUES = ((@options.duration / MS_PER_FRAME) / FRAMES_PER_VALUE) | 0

    @resetFormat()

    @value = @cleanValue(@options.value ? '')

    @renderInside()
    @render()

    try
      for property in ['innerHTML', 'innerText', 'textContent'] when @el[property]?
        do (property) =>
          Object.defineProperty @el, property,
            get: =>
              if property is 'innerHTML'
                @inside.outerHTML
              else
                # It's just a single HTML element, so innerText is the
                # same as outerText.
                @inside.innerText ? @inside.textContent
            set: (val) =>
              @update val
    catch e
      # Safari
      @watchForMutations()

    @

  renderInside: ->
    @inside = document.createElement 'div'
    @inside.className = 'odometer-inside'
    @el.innerHTML = ''
    @el.appendChild @inside

  watchForMutations: ->
    # Safari doesn't allow us to wrap .innerHTML, so we listen for it
    # changing.
    return unless MutationObserver?

    try
      @observer ?= new MutationObserver (mutations) =>
        newVal = @el.innerText

        @renderInside()
        @render @value
        @update newVal

      @watchMutations = true
      @startWatchingMutations()
    catch e

  startWatchingMutations: ->
    if @watchMutations
      @observer.observe @el, {childList: true}

  stopWatchingMutations: ->
    @observer?.disconnect()

  cleanValue: (val) ->
    if typeof val is 'string'
      # We need to normalize the format so we can properly turn it into
      # a float.
      val = val.replace((@format.radix ? '.'), '<radix>')
      val = val.replace /[.,]/g, ''
      val = val.replace '<radix>', '.'
      val = parseFloat(val, 10) or 0

    round(val, @format.precision)

  bindTransitionEnd: ->
    return if @transitionEndBound
    @transitionEndBound = true

    # The event will be triggered once for each ribbon, we only
    # want one render though
    renderEnqueued = false
    for event in TRANSITION_END_EVENTS.split(' ')
      @el.addEventListener event, =>
        return true if renderEnqueued

        renderEnqueued = true

        setTimeout =>
          @render()
          renderEnqueued = false

          trigger @el, 'odometerdone'
        , 0

        true
      , false

  resetFormat: ->
    format = @options.format ? DIGIT_FORMAT
    format or= 'd'

    parsed = FORMAT_PARSER.exec format
    if not parsed
      throw new Error "Odometer: Unparsable digit format"

    [repeating, radix, fractional] = parsed[1..3]

    precision = fractional?.length or 0

    @format = {repeating, radix, precision}

  render: (value=@value) ->
    @stopWatchingMutations()
    @resetFormat()

    @inside.innerHTML = ''

    theme = @options.theme

    classes = @el.className.split(' ')
    newClasses = []
    for cls in classes when cls.length
      if match = /^odometer-theme-(.+)$/.exec(cls)
        theme = match[1]
        continue

      if /^odometer(-|$)/.test(cls)
        continue

      newClasses.push cls

    newClasses.push 'odometer'

    unless TRANSITION_SUPPORT
      newClasses.push 'odometer-no-transitions'

    if theme
      newClasses.push "odometer-theme-#{ theme }"
    else
      # This class matches all themes, so it should do what you'd expect if only one
      # theme css file is brought into the page.
      newClasses.push "odometer-auto-theme"

    @el.className = newClasses.join(' ')

    @ribbons = {}

    @formatDigits(value)

    @startWatchingMutations()

  formatDigits: (value) ->
    @digits = []

    if @options.formatFunction
      valueString = @options.formatFunction(value)
      for valueDigit in valueString.split('').reverse()
        if valueDigit.match(/0-9/)
          digit = @renderDigit()
          digit.querySelector('.odometer-value').innerHTML = valueDigit
          @digits.push digit
          @insertDigit digit
        else
          @addSpacer valueDigit
    else
      wholePart = not @format.precision or not fractionalPart(value) or false
      for digit in value.toString().split('').reverse()
        if digit is '.'
          wholePart = true

        @addDigit digit, wholePart

    return

  update: (newValue) ->
    newValue = @cleanValue newValue

    return unless diff = newValue - @value

    removeClass @el, 'odometer-animating-up odometer-animating-down odometer-animating'
    if diff > 0
      addClass @el, 'odometer-animating-up'
    else
      addClass @el, 'odometer-animating-down'

    @stopWatchingMutations()
    @animate newValue
    @startWatchingMutations()

    setTimeout =>
      # Force a repaint
      @el.offsetHeight

      addClass @el, 'odometer-animating'
    , 0

    @value = newValue

  renderDigit: ->
    createFromHTML DIGIT_HTML

  insertDigit: (digit, before) ->
    if before?
      @inside.insertBefore digit, before
    else if not @inside.children.length
      @inside.appendChild digit
    else
      @inside.insertBefore digit, @inside.children[0]

  addSpacer: (chr, before, extraClasses) ->
    spacer = createFromHTML FORMAT_MARK_HTML
    spacer.innerHTML = chr
    addClass(spacer, extraClasses) if extraClasses
    @insertDigit spacer, before

  addDigit: (value, repeating=true) ->
    if value is '-'
      return @addSpacer value, null, 'odometer-negation-mark'

    if value is '.'
      return @addSpacer (@format.radix ? '.'), null, 'odometer-radix-mark'

    if repeating
      resetted = false
      while true
        if not @format.repeating.length
          if resetted
            throw new Error "Bad odometer format without digits"

          @resetFormat()
          resetted = true

        chr = @format.repeating[@format.repeating.length - 1]
        @format.repeating = @format.repeating.substring(0, @format.repeating.length - 1)

        break if chr is 'd'

        @addSpacer chr

    digit = @renderDigit()
    digit.querySelector('.odometer-value').innerHTML = value
    @digits.push digit

    @insertDigit digit

  animate: (newValue) ->
    if not TRANSITION_SUPPORT or @options.animation is 'count'
      @animateCount newValue
    else
      @animateSlide newValue

  animateCount: (newValue) ->
    return unless diff = +newValue - @value

    start = last = now()

    cur = @value
    do tick = =>
      if (now() - start) > @options.duration
        @value = newValue
        @render()
        trigger @el, 'odometerdone'
        return

      delta = now() - last

      if delta > COUNT_MS_PER_FRAME
        last = now()

        fraction = delta / @options.duration
        dist = diff * fraction

        cur += dist
        @render Math.round cur

      if requestAnimationFrame?
        requestAnimationFrame tick
      else
        setTimeout tick, COUNT_MS_PER_FRAME

  getDigitCount: (values...) ->
    for value, i in values
      values[i] = Math.abs(value)

    max = Math.max values...

    Math.ceil(Math.log(max + 1) / Math.log(10))

  getFractionalDigitCount: (values...) ->
    # This assumes the value has already been rounded to
    # @format.precision places
    #
    parser = /^\-?\d*\.(\d*?)0*$/
    for value, i in values
      values[i] = value.toString()

      parts = parser.exec values[i]

      if not parts?
        values[i] = 0
      else
        values[i] = parts[1].length

    Math.max values...

  resetDigits: ->
    @digits = []
    @ribbons = []
    @inside.innerHTML = ''
    @resetFormat()

  animateSlide: (newValue) ->
    oldValue = @value

    fractionalCount = @getFractionalDigitCount oldValue, newValue

    if fractionalCount
      newValue = newValue * Math.pow(10, fractionalCount)
      oldValue = oldValue * Math.pow(10, fractionalCount)

    return unless diff = newValue - oldValue

    @bindTransitionEnd()

    digitCount = @getDigitCount(oldValue, newValue)

    digits = []
    boosted = 0
    # We create a array to represent the series of digits which should be
    # animated in each column
    for i in [0...digitCount]
      start = truncate(oldValue  / Math.pow(10, (digitCount - i - 1)))
      end = truncate(newValue / Math.pow(10, (digitCount - i - 1)))

      dist = end - start

      if Math.abs(dist) > @MAX_VALUES
        # We need to subsample
        frames = []

        # Subsequent digits need to be faster than previous ones
        incr = dist / (@MAX_VALUES + @MAX_VALUES * boosted * DIGIT_SPEEDBOOST)
        cur = start

        while (dist > 0 and cur < end) or (dist < 0 and cur > end)
          frames.push Math.round cur
          cur += incr

        if frames[frames.length - 1] isnt end
          frames.push end

        boosted++
      else
        frames = [start..end]

      # We only care about the last digit
      for frame, i in frames
        frames[i] = Math.abs(frame % 10)

      digits.push frames

    @resetDigits()

    for frames, i in digits.reverse()
      if not @digits[i]
        @addDigit ' ', (i >= fractionalCount)

      @ribbons[i] ?= @digits[i].querySelector('.odometer-ribbon-inner')
      @ribbons[i].innerHTML = ''

      if diff < 0
        frames = frames.reverse()

      for frame, j in frames
        numEl = document.createElement('div')
        numEl.className = 'odometer-value'
        numEl.innerHTML = frame

        @ribbons[i].appendChild numEl

        if j == frames.length - 1
          addClass numEl, 'odometer-last-value'
        if j == 0
          addClass numEl, 'odometer-first-value'

    if start < 0
      @addDigit '-'

    mark = @inside.querySelector('.odometer-radix-mark')
    mark.parent.removeChild(mark) if mark?

    if fractionalCount
      @addSpacer @format.radix, @digits[fractionalCount - 1], 'odometer-radix-mark'

Odometer.options = window.odometerOptions ? {}

setTimeout ->
  # We do this in a seperate pass to allow people to set
  # window.odometerOptions after bringing the file in.
  if window.odometerOptions
    for k, v of window.odometerOptions
      Odometer.options[k] ?= v
, 0

Odometer.init = ->
  if not document.querySelectorAll?
    # IE 7 or 8 in Quirksmode
    return

  elements = document.querySelectorAll (Odometer.options.selector or '.odometer')

  for el in elements
    el.odometer = new Odometer {el, value: (el.innerText ? el.textContent)}

if document.documentElement?.doScroll? and document.createEventObject?
  # IE < 9
  _old = document.onreadystatechange
  document.onreadystatechange = ->
    if document.readyState is 'complete' and Odometer.options.auto isnt false
      Odometer.init()

    _old?.apply this, arguments
else
  document.addEventListener 'DOMContentLoaded', ->
    if Odometer.options.auto isnt false
      Odometer.init()
  , false


if typeof define is 'function' and define.amd
  # AMD. Register as an anonymous module.
  define [], ->
    Odometer
else if exports?
  # CommonJS
  module.exports = Odometer
else
  # Browser globals
  window.Odometer = Odometer
