if not Tether?
  throw new Error "You must include the utils.js file before tether.js"

{getScrollParent, getSize, getOuterSize, getBounds, getOffsetParent, extend, addClass, removeClass, updateClasses, defer, flush} = Tether.Utils

within = (a, b, diff=1) ->
  a + diff >= b >= a - diff

transformKey = do ->
  el = document.createElement 'div'
  
  for key in ['transform', 'webkitTransform', 'OTransform', 'MozTransform', 'msTransform']
    if el.style[key] isnt undefined
      return key

tethers = []

position = ->
  for tether in tethers
    tether.position(false)

  flush()

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

do ->
  lastCall = null
  lastDuration = null
  pendingTimeout = null

  tick = ->
    if lastDuration? and lastDuration > 16
      # We voluntarily throttle ourselves if we can't manage 60fps
      lastDuration = Math.min(lastDuration - 16, 250)

      # Just in case this is the last event, remember to position just once more
      pendingTimeout = setTimeout tick, 250
      return

    if lastCall? and (now() - lastCall) < 10
      # Some browsers call events a little too frequently, refuse to run more than is reasonable
      return

    if pendingTimeout?
      clearTimeout pendingTimeout
      pendingTimeout = null

    lastCall = now()
    
    position()

    lastDuration = now() - lastCall
    
  for event in ['resize', 'scroll']
    window.addEventListener event, tick

MIRROR_LR =
  center: 'center'
  left: 'right'
  right: 'left'

MIRROR_TB =
  middle: 'middle'
  top: 'bottom'
  bottom: 'top'

OFFSET_MAP =
  top: 0
  left: 0
  middle: '50%'
  center: '50%'
  bottom: '100%'
  right: '100%'

autoToFixedAttachment = (attachment, relativeToAttachment) ->
  {left, top} = attachment

  if left is 'auto'
    left = MIRROR_LR[relativeToAttachment.left]

  if top is 'auto'
    top = MIRROR_TB[relativeToAttachment.top]

  {left, top}

attachmentToOffset = (attachment) ->
  return {
    left: OFFSET_MAP[attachment.left] ? attachment.left
    top: OFFSET_MAP[attachment.top] ? attachment.top
  }

addOffset = (offsets...) ->
  out = {top: 0, left: 0}

  for {top, left} in offsets
    if typeof top is 'string'
      top = parseFloat(top, 10)
    if typeof left is 'string'
      left = parseFloat(left, 10)

    out.top += top
    out.left += left

  out

offsetToPx = (offset, size) ->
  if typeof offset.left is 'string' and offset.left.indexOf('%') isnt -1
    offset.left = parseFloat(offset.left, 10) / 100 * size.width
  if typeof offset.top is 'string' and offset.top.indexOf('%') isnt -1
    offset.top = parseFloat(offset.top, 10) / 100 * size.height

  offset

parseAttachment = parseOffset = (value) ->
  [top, left] = value.split(' ')

  {top, left}

class _Tether
  @modules: []

  constructor: (options) ->
    tethers.push @

    @history = []

    @setOptions options, false

    for module in Tether.modules
      module.initialize?.call(@)

    @position()

  getClass: (key) ->
    if @options.classes?[key]
      @options.classes[key]
    else if @options.classes?[key] isnt false
      if @options.classPrefix
        "#{ @options.classPrefix }-#{ key }"
      else
        key
    else
      ''

  setOptions: (@options, position=true) ->
    defaults =
      offset: '0 0'
      targetOffset: '0 0'
      targetAttachment: 'auto auto'
      classPrefix: 'tether'

    @options = extend defaults, @options
      
    {@element, @target, @targetModifier} = @options

    if @target is 'viewport'
      @target = document.body
      @targetModifier = 'visible'
    else if @target is 'scroll-handle'
      @target = document.body
      @targetModifier = 'scroll-handle'

    for key in ['element', 'target']
      if not @[key]?
        throw new Error "Tether Error: Both element and target must be defined"

      if @[key].jquery?
        @[key] = @[key][0]
      else if typeof @[key] is 'string'
        @[key] = document.querySelector @[key]

    addClass @element, @getClass 'element'
    addClass @target, @getClass 'target'

    if not @options.attachment
      throw new Error "Tether Error: You must provide an attachment"

    @targetAttachment = parseAttachment @options.targetAttachment
    @attachment = parseAttachment @options.attachment
    @offset = parseOffset @options.offset
    @targetOffset = parseOffset @options.targetOffset

    if @scrollParent?
      @disable()

    if @targetModifier is 'scroll-handle'
      @scrollParent = @target
    else
      @scrollParent = getScrollParent @target

    unless @options.enabled is false
      @enable(position)

  getTargetBounds: ->
    if @targetModifier?
      switch @targetModifier
        when 'visible'
          if @target is document.body
            {top: pageYOffset, left: pageXOffset, height: innerHeight, width: innerWidth}
          else
            bounds = getBounds @target

            out =
              height: bounds.height
              width: bounds.width
              top: bounds.top
              left: bounds.left

            out.height = Math.min(out.height, bounds.height - (pageYOffset - bounds.top))
            out.height = Math.min(out.height, bounds.height - ((bounds.top + bounds.height) - (pageYOffset + innerHeight)))
            out.height = Math.min(innerHeight, out.height)
            out.height -= 2

            out.width = Math.min(out.width, bounds.width - (pageXOffset - bounds.left))
            out.width = Math.min(out.width, bounds.width - ((bounds.left + bounds.width) - (pageXOffset + innerWidth)))
            out.width = Math.min(innerWidth, out.width)
            out.width -= 2

            if out.top < pageYOffset
              out.top = pageYOffset
            if out.left < pageXOffset
              out.left = pageXOffset

            out

        when 'scroll-handle'
          target = @target
          if target is document.body
            target = document.documentElement

            bounds =
              left: pageXOffset
              top: pageYOffset
              height: innerHeight
              width: innerWidth
          else
            bounds = getBounds target

          style = getComputedStyle target

          hasBottomScroll = target.scrollWidth > target.clientWidth or 'scroll' is [style.overflow, style.overflowX] or @target isnt document.body

          scrollBottom = 0
          if hasBottomScroll
            scrollBottom = 15

          height = bounds.height - parseFloat(style.borderTopWidth) - parseFloat(style.borderBottomWidth) - scrollBottom

          out =
            width: 15
            height: height * 0.975 * (height / target.scrollHeight)
            left: bounds.left + bounds.width - parseFloat(style.borderLeftWidth) - 15

          fitAdj = 0
          if height < 408 and @target is document.body
            fitAdj = -0.00011 * Math.pow(height, 2) - 0.00727 * height + 22.58

          if @target isnt document.body
            out.height = Math.max out.height, 24

          scrollPercentage = target.scrollTop / (target.scrollHeight - height)
          out.top = scrollPercentage * (height - out.height - fitAdj) + bounds.top + parseFloat(style.borderTopWidth)

          if @target is document.body
            out.height = Math.max out.height, 24

          out
    else
      getBounds @target

  clearCache: ->
    @_cache = {}

  cache: (k, getter) ->
    # More than one module will often need the same DOM info, so
    # we keep a cache which is cleared on each position call
    @_cache ?= {}

    if not @_cache[k]?
      @_cache[k] = getter.call(@)

    @_cache[k]

  enable: (position=true) ->
    addClass @target, @getClass 'enabled'
    addClass @element, @getClass 'enabled'
    @enabled = true

    if @scrollParent isnt document
      @scrollParent.addEventListener 'scroll', @position

    if position
      @position()

  disable: ->
    removeClass @target, @getClass 'enabled'
    removeClass @element, @getClass 'enabled'
    @enabled = false

    if @scrollParent?
      @scrollParent.removeEventListener 'scroll', @position

  destroy: ->
    @disable()

    for tether, i in tethers
      if tether is @
        tethers.splice i, 1
        break

  updateAttachClasses: (elementAttach=@attachment, targetAttach=@targetAttachment) ->
    sides = ['left', 'top', 'bottom', 'right', 'middle', 'center']

    if @_addAttachClasses?.length
        # updateAttachClasses can be called more than once in a position call, so
        # we need to clean up after ourselves such that when the last defer gets
        # ran it doesn't add any extra classes from previous calls.
        @_addAttachClasses.splice 0, @_addAttachClasses.length

    add = @_addAttachClasses ?= []
    add.push "#{ @getClass('element-attached') }-#{ elementAttach.top }" if elementAttach.top
    add.push "#{ @getClass('element-attached') }-#{ elementAttach.left }" if elementAttach.left
    add.push "#{ @getClass('target-attached') }-#{ targetAttach.top }" if targetAttach.top
    add.push "#{ @getClass('target-attached') }-#{ targetAttach.left }" if targetAttach.left

    all = []
    all.push "#{ @getClass('element-attached') }-#{ side }" for side in sides
    all.push "#{ @getClass('target-attached') }-#{ side }" for side in sides

    defer =>
      return unless @_addAttachClasses?

      updateClasses @element, @_addAttachClasses, all
      updateClasses @target, @_addAttachClasses, all

      @_addAttachClasses = undefined

  position: (flushChanges=true) =>
    # flushChanges commits the changes immediately, leave true unless you are positioning multiple
    # tethers (in which case call Tether.Utils.flush yourself when you're done)

    return unless @enabled

    @clearCache()

    # Turn 'auto' attachments into the appropriate corner or edge
    targetAttachment = autoToFixedAttachment(@targetAttachment, @attachment)

    @updateAttachClasses @attachment, targetAttachment

    elementPos = @cache 'element-bounds', => getBounds @element
    {width, height} = elementPos

    if width is 0 and height is 0 and @lastSize?
      # We cache the height and width to make it possible to position elements that are
      # getting hidden.
      {width, height} = @lastSize
    else
      @lastSize = {width, height}

    targetSize = targetPos = @cache 'target-bounds', => @getTargetBounds()

    # Get an actual px offset from the attachment
    offset = offsetToPx attachmentToOffset(@attachment), {width, height}
    targetOffset = offsetToPx attachmentToOffset(targetAttachment), targetSize

    manualOffset = offsetToPx(@offset, {width, height})
    manualTargetOffset = offsetToPx(@targetOffset, targetSize)

    # Add the manually provided offset
    offset = addOffset offset, manualOffset
    targetOffset = addOffset targetOffset, manualTargetOffset

    # It's now our goal to make (element position + offset) == (target position + target offset)
    left = targetPos.left + targetOffset.left - offset.left
    top = targetPos.top + targetOffset.top - offset.top

    for module in Tether.modules
      ret = module.position.call(@, {left, top, targetAttachment, targetPos, elementPos, offset, targetOffset, manualOffset, manualTargetOffset})

      if not ret? or typeof ret isnt 'object'
        continue
      else if ret is false
        return false
      else
        {top, left} = ret

    # We describe the position three different ways to give the optimizer
    # a chance to decide the best possible way to position the element
    # with the fewest repaints.
    next = {
      # It's position relative to the page (absolute positioning when
      # the element is a child of the body)
      page:
        top: top
        bottom: document.body.scrollHeight - top - height
        left: left
        right: document.body.scrollWidth - left - width

      # It's position relative to the viewport (fixed positioning)
      viewport:
        top: top - pageYOffset
        bottom: pageYOffset - top - height + innerHeight
        left: left - pageXOffset
        right: pageXOffset - left - width + innerWidth
    }

    if @options.optimizations?.moveElement isnt false and not @targetModifier?
      offsetParent = @cache 'target-offsetparent', => getOffsetParent @target
      offsetPosition = @cache 'target-offsetparent-bounds', -> getBounds offsetParent
      offsetParentStyle = getComputedStyle offsetParent
      elementStyle = getComputedStyle @element
      offsetParentSize = offsetPosition

      offsetBorder = {}
      for side in ['top', 'left', 'bottom', 'right']
        offsetBorder[side] = parseFloat offsetParentStyle["border-#{ side }-width"]

      offsetPosition.right = document.body.scrollWidth - offsetPosition.left - offsetParentSize.width + offsetBorder.right
      offsetPosition.bottom = document.body.scrollHeight - offsetPosition.top - offsetParentSize.height + offsetBorder.bottom

      if next.page.top >= (offsetPosition.top + offsetBorder.top) and next.page.bottom >= offsetPosition.bottom
        if next.page.left >= (offsetPosition.left + offsetBorder.left) and next.page.right >= offsetPosition.right
          # We're within the visible part of the target's scroll parent

          scrollTop = offsetParent.scrollTop
          scrollLeft = offsetParent.scrollLeft

          # It's position relative to the target's offset parent (absolute positioning when
          # the element is moved to be a child of the target's offset parent).
          next.offset =
            top: next.page.top - offsetPosition.top + scrollTop - offsetBorder.top
            left: next.page.left - offsetPosition.left + scrollLeft - offsetBorder.left

    # We could also travel up the DOM and try each containing context, rather than only
    # looking at the body, but we're gonna get diminishing returns.

    @move next

    @history.unshift next

    if @history.length > 3
      @history.pop()

    if flushChanges
      flush()

    true

  move: (position) ->
    return if not @element.parentNode?

    same = {}

    for type of position
      same[type] = {}

      for key of position[type]
        found = false

        for point in @history
          unless within(point[type]?[key], position[type][key])
            found = true
            break

        if not found
          same[type][key] = true
     
    css = {top: '', left: '', right: '', bottom: ''}

    transcribe = (same, pos) =>
      if @options.optimizations?.gpu isnt false
        if same.top
          css.top = 0
          yPos = pos.top
        else
          css.bottom = 0
          yPos = -pos.bottom

        if same.left
          css.left = 0
          xPos = pos.left
        else
          css.right = 0
          xPos = -pos.right

        
        css[transformKey] = "translateX(#{ Math.round xPos }px) translateY(#{ Math.round yPos }px)"

        if transformKey isnt 'msTransform'
          # The Z transform will keep this in the GPU (faster, and prevents artifacts),
          # but IE9 doesn't support 3d transforms and will choke.
          css[transformKey] += " translateZ(0)"

      else
        if same.top
          css.top = "#{ pos.top }px"
        else
          css.bottom = "#{ pos.bottom }px"

        if same.left
          css.left = "#{ pos.left }px"
        else
          css.right = "#{ pos.right }px"

    moved = false
    if (same.page.top or same.page.bottom) and (same.page.left or same.page.right)
      css.position = 'absolute'
      transcribe same.page, position.page

    else if (same.viewport.top or same.viewport.bottom) and (same.viewport.left or same.viewport.right)
      css.position = 'fixed'
      transcribe same.viewport, position.viewport

    else if same.offset? and same.offset.top and same.offset.left
      css.position = 'absolute'

      offsetParent = @cache 'target-offsetparent', => getOffsetParent @target

      if getOffsetParent(@element) isnt offsetParent
        defer =>
          @element.parentNode.removeChild @element
          offsetParent.appendChild @element

      transcribe same.offset, position.offset

      moved = true
      
    else
      css.position = 'absolute'
      transcribe {top: true, left: true}, position.page

    if not moved and @element.parentNode.tagName isnt 'BODY'
      @element.parentNode.removeChild @element
      document.body.appendChild @element

    # Any css change will trigger a repaint, so let's avoid one if nothing changed
    writeCSS = {}
    write = false
    for key, val of css
      elVal = @element.style[key]

      if elVal isnt '' and val isnt '' and key in ['top', 'left', 'bottom', 'right']
        elVal = parseFloat elVal
        val = parseFloat val

      if elVal isnt val
        write = true
        writeCSS[key] = css[key]

    if write
      defer =>
        extend @element.style, writeCSS

Tether.position = position

window.Tether = extend _Tether, Tether
