# This is injected into each page that is loaded

class PoltergeistAgent
  # Since this code executes in the sites browser space - copy needed JSON functions
  # in case user code messes with JSON (early mootools for instance)
  @.JSON ||= { parse: JSON.parse, stringify: JSON.stringify }

  constructor: ->
    @elements = []
    @nodes    = {}

  externalCall: (name, args) ->
    try
      { value: this[name].apply(this, args) }
    catch error
      { error: { message: error.toString(), stack: error.stack } }

  @stringify: (object) ->
    try
      PoltergeistAgent.JSON.stringify object, (key, value) ->
        if Array.isArray(this[key])
          return this[key]
        else
          return value
    catch error
      if error instanceof TypeError
        '"(cyclic structure)"'
      else
        throw error

  # Somehow PhantomJS returns all characters(brackets, etc) properly encoded
  # except whitespace character in pathname part of the location. This hack
  # is intended to fix this up.
  currentUrl: ->
    window.location.href.replace(/\ /g, '%20')

  find: (method, selector, within = document) ->
    try
      if method == "xpath"
        xpath   = document.evaluate(selector, within, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null)
        results = (xpath.snapshotItem(i) for i in [0...xpath.snapshotLength])
      else
        results = within.querySelectorAll(selector)

      this.register(el) for el in results
    catch error
      # DOMException.INVALID_EXPRESSION_ERR is undefined, using pure code
      if error.code == DOMException.SYNTAX_ERR || error.code == 51
        throw new PoltergeistAgent.InvalidSelector
      else
        throw error

  register: (element) ->
    @elements.push(element)
    @elements.length - 1

  documentSize: ->
    height: document.documentElement.scrollHeight || document.documentElement.clientHeight,
    width:  document.documentElement.scrollWidth  || document.documentElement.clientWidth

  get: (id) ->
    @nodes[id] or= new PoltergeistAgent.Node(this, @elements[id])

  nodeCall: (id, name, args) ->
    node = this.get(id)
    throw new PoltergeistAgent.ObsoleteNode if node.isObsolete()
    node[name].apply(node, args)

  beforeUpload: (id) ->
    this.get(id).setAttribute('_poltergeist_selected', '')

  afterUpload: (id) ->
    this.get(id).removeAttribute('_poltergeist_selected')

  clearLocalStorage: ->
    localStorage.clear()

class PoltergeistAgent.ObsoleteNode
  toString: -> "PoltergeistAgent.ObsoleteNode"

class PoltergeistAgent.InvalidSelector
  toString: -> "PoltergeistAgent.InvalidSelector"

class PoltergeistAgent.Node
  @EVENTS = {
    FOCUS: ['blur', 'focus', 'focusin', 'focusout'],
    MOUSE: ['click', 'dblclick', 'mousedown', 'mouseenter', 'mouseleave', 'mousemove',
            'mouseover', 'mouseout', 'mouseup', 'contextmenu'],
    FORM: ['submit']
  }

  constructor: (@agent, @element) ->

  parentId: ->
    @agent.register(@element.parentNode)

  parentIds: ->
    ids = []
    parent = @element.parentNode
    while parent != document
      ids.push @agent.register(parent)
      parent = parent.parentNode
    ids

  find: (method, selector) ->
    @agent.find(method, selector, @element)

  isObsolete: ->
    obsolete = (element) =>
      if (parent = element.parentNode)?
        if parent == document
          return false
        else
          obsolete parent
      else
        return true
    obsolete @element

  changed: ->
    event = document.createEvent('HTMLEvents')
    event.initEvent('change', true, false)

    # In the case of an OPTION tag, the change event should come
    # from the parent SELECT
    if @element.nodeName == 'OPTION'
      element = @element.parentNode
      element = element.parentNode if element.nodeName == 'OPTGROUP'
      element
    else
      element = @element

    element.dispatchEvent(event)

  input: ->
    event = document.createEvent('HTMLEvents')
    event.initEvent('input', true, false)
    @element.dispatchEvent(event)

  keyupdowned: (eventName, keyCode) ->
    event = document.createEvent('UIEvents')
    event.initEvent(eventName, true, true)
    event.keyCode  = keyCode
    event.which    = keyCode
    event.charCode = 0
    @element.dispatchEvent(event)

  keypressed: (altKey, ctrlKey, shiftKey, metaKey, keyCode, charCode) ->
    event = document.createEvent('UIEvents')
    event.initEvent('keypress', true, true)
    event.window   = @agent.window
    event.altKey   = altKey
    event.ctrlKey  = ctrlKey
    event.shiftKey = shiftKey
    event.metaKey  = metaKey
    event.keyCode  = keyCode
    event.charCode = charCode
    event.which    = keyCode
    @element.dispatchEvent(event)

  insideBody: ->
    @element == document.body ||
    document.evaluate('ancestor::body', @element, null, XPathResult.BOOLEAN_TYPE, null).booleanValue

  allText: ->
    @element.textContent

  visibleText: ->
    if this.isVisible()
      if @element.nodeName == "TEXTAREA"
        @element.textContent
      else
        if @element instanceof SVGElement
          @element.textContent
        else
          @element.innerText

  deleteText: ->
    range = document.createRange()
    range.selectNodeContents(@element)
    window.getSelection().removeAllRanges()
    window.getSelection().addRange(range)
    window.getSelection().deleteFromDocument()

  getProperty: (name) ->
    @element[name]

  getAttributes: ->
    attrs = {}
    for attr in @element.attributes
      attrs[attr.name] = attr.value.replace("\n","\\n");
    attrs

  getAttribute: (name) ->
    if name == 'checked' || name == 'selected'
      @element[name]
    else
      @element.getAttribute(name)

  scrollIntoView: ->
    @element.scrollIntoViewIfNeeded()
    #Sometimes scrollIntoViewIfNeeded doesn't seem to work, not really sure why.
    #Just calling scrollIntoView doesnt work either, however calling scrollIntoView
    #after scrollIntoViewIfNeeded when element is not in the viewport does appear to work
    @element.scrollIntoView() unless this.isInViewport()

  value: ->
    if @element.tagName == 'SELECT' && @element.multiple
      option.value for option in @element.children when option.selected
    else
      @element.value

  set: (value) ->
    return if @element.readOnly

    if (@element.maxLength >= 0)
      value = value.substr(0, @element.maxLength)

    this.trigger('focus')
    @element.value = ''

    if @element.type == 'number'
      @element.value = value
    else
      for char in value
        keyCode = this.characterToKeyCode(char)
        this.keyupdowned('keydown', keyCode)
        @element.value += char

        this.keypressed(false, false, false, false, char.charCodeAt(0), char.charCodeAt(0))
        this.keyupdowned('keyup', keyCode)

    this.changed()
    this.input()
    this.trigger('blur')

  isMultiple: ->
    @element.multiple

  setAttribute: (name, value) ->
    @element.setAttribute(name, value)

  removeAttribute: (name) ->
    @element.removeAttribute(name)

  select: (value) ->
    if @isDisabled()
      false
    else if value == false && !@element.parentNode.multiple
      false
    else
      this.trigger('focus', @element.parentNode)

      @element.selected = value
      this.changed()

      this.trigger('blur', @element.parentNode)
      true

  tagName: ->
    @element.tagName

  isVisible: (element = @element) ->
    #if an area element, check visibility of relevant image
    if element.tagName == 'AREA'
      map_name = document.evaluate('./ancestor::map/@name', element, null, XPathResult.STRING_TYPE, null).stringValue
      element = document.querySelector("img[usemap='##{map_name}']")
      return false unless element?

    while (element)
      style = window.getComputedStyle(element)
      return false if style.display == 'none' or
                      style.visibility == 'hidden' or
                      parseFloat(style.opacity) == 0
      element = element.parentElement

    return true

  isInViewport: ->
    rect = @element.getBoundingClientRect();

    rect.top >= 0 &&
    rect.left >= 0 &&
    rect.bottom <= window.innerHeight &&
    rect.right <= window.innerWidth

  isDisabled: ->
    @element.disabled || @element.tagName == 'OPTION' && @element.parentNode.disabled

  path: ->
    elements = @parentIds().reverse().map((id) => @agent.get(id))
    elements.push(this)
    selectors = elements.map (el)->
      prev_siblings = el.find('xpath', "./preceding-sibling::#{el.tagName()}")
      "#{el.tagName()}[#{prev_siblings.length + 1}]"
    "//" + selectors.join('/')

  containsSelection: ->
    selectedNode = document.getSelection().focusNode

    return false if !selectedNode

    if selectedNode.nodeType == 3
      selectedNode = selectedNode.parentNode

    @element.contains(selectedNode)

  frameOffset: ->
    win    = window
    offset = { top: 0, left: 0 }

    while win.frameElement
      rect  = win.frameElement.getClientRects()[0]
      style = win.getComputedStyle(win.frameElement)
      win   = win.parent

      offset.top  += rect.top + parseInt(style.getPropertyValue("padding-top"), 10)
      offset.left += rect.left + parseInt(style.getPropertyValue("padding-left"), 10)

    offset

  position: ->
    # Elements inside an SVG return underfined for getClientRects???
    rect = @element.getClientRects()[0] || @element.getBoundingClientRect()
    throw new PoltergeistAgent.ObsoleteNode unless rect
    frameOffset = this.frameOffset()

    pos = {
      top:    rect.top    + frameOffset.top,
      right:  rect.right  + frameOffset.left,
      left:   rect.left   + frameOffset.left,
      bottom: rect.bottom + frameOffset.top,
      width:  rect.width,
      height: rect.height
    }

    pos

  trigger: (name, element = @element) ->
    if Node.EVENTS.MOUSE.indexOf(name) != -1
      event = document.createEvent('MouseEvent')
      event.initMouseEvent(
        name, true, true, window, 0, 0, 0, 0, 0,
        false, false, false, false, 0, null
      )
    else if Node.EVENTS.FOCUS.indexOf(name) != -1
      event = this.obtainEvent(name)
    else if Node.EVENTS.FORM.indexOf(name) != -1
      event = this.obtainEvent(name)
    else
      throw "Unknown event"

    element.dispatchEvent(event)

  obtainEvent: (name) ->
    event = document.createEvent('HTMLEvents')
    event.initEvent(name, true, true)
    event

  mouseEventTest: (x, y) ->
    frameOffset = this.frameOffset()

    x -= frameOffset.left
    y -= frameOffset.top

    el = origEl = document.elementFromPoint(x, y)

    while el
      if el == @element
        return { status: 'success' }
      else
        el = el.parentNode

    { status: 'failure', selector: origEl && this.getSelector(origEl) }
  getSelector: (el) ->
    selector = if el.tagName != 'HTML' then this.getSelector(el.parentNode) + ' ' else ''
    selector += el.tagName.toLowerCase()
    selector += "##{el.id}" if el.id

    #PhantomJS < 2.0 doesn't support classList for SVG elements - so get classes manually
    classes = el.classList || (el.getAttribute('class')?.trim()?.split(/\s+/)) || []
    for className in classes when className != ''
      selector += ".#{className}"
    selector

  characterToKeyCode: (character) ->
    code = character.toUpperCase().charCodeAt(0)
    specialKeys =
      96: 192  #`
      45: 189  #-
      61: 187  #=
      91: 219  #[
      93: 221  #]
      92: 220  #\
      59: 186  #;
      39: 222  #'
      44: 188  #,
      46: 190  #.
      47: 191  #/
      127: 46  #delete
      126: 192 #~
      33: 49   #!
      64: 50   #@
      35: 51   ##
      36: 52   #$
      37: 53   #%
      94: 54   #^
      38: 55   #&
      42: 56   #*
      40: 57   #(
      41: 48   #)
      95: 189  #_
      43: 187  #+
      123: 219 #{
      125: 221 #}
      124: 220 #|
      58: 186  #:
      34: 222  #"
      60: 188  #<
      62: 190  #>
      63: 191 #?

    specialKeys[code] || code

  isDOMEqual: (other_id) ->
    @element == @agent.get(other_id).element

window.__poltergeist = new PoltergeistAgent

document.addEventListener(
  'DOMContentLoaded',
  -> console.log('__DOMContentLoaded')
)
