Response = TurboGraft.Response TurboHead = TurboGraft.TurboHead jQuery = window.jQuery

xhr = null activeDocument = document

installDocumentReadyPageEventTriggers = ->

activeDocument.addEventListener 'DOMContentLoaded', ( ->
  triggerEvent 'page:change'
  triggerEvent 'page:update'
), true

installJqueryAjaxSuccessPageUpdateTrigger = ->

if typeof jQuery isnt 'undefined'
  jQuery(activeDocument).on 'ajaxSuccess', (event, xhr, settings) ->
    return unless jQuery.trim xhr.responseText
    triggerEvent 'page:update'

# Handle bug in Firefox 26/27 where history.state is initially undefined historyStateIsDefined =

window.history.state != undefined or navigator.userAgent.match /Firefox\/2[6|7]/

browserSupportsPushState =

window.history and window.history.pushState and window.history.replaceState and historyStateIsDefined

window.triggerEvent = (name, data) ->

event = activeDocument.createEvent 'Events'
event.data = data if data
event.initEvent name, true, true
activeDocument.dispatchEvent event

window.triggerEventFor = (name, node, data) ->

event = activeDocument.createEvent 'Events'
event.data = data if data
event.initEvent name, true, true
node.dispatchEvent event

popCookie = (name) ->

value = activeDocument.cookie.match(new RegExp(name+"=(\\w+)"))?[1].toUpperCase() or ''
activeDocument.cookie = name + '=; expires=Thu, 01-Jan-70 00:00:01 GMT; path=/'
value

requestMethodIsSafe =

popCookie('request_method') in ['GET','']

browserSupportsTurbolinks = browserSupportsPushState and requestMethodIsSafe

browserSupportsCustomEvents =

activeDocument.addEventListener and activeDocument.createEvent

if browserSupportsCustomEvents

installDocumentReadyPageEventTriggers()
installJqueryAjaxSuccessPageUpdateTrigger()

replaceNode = (newNode, oldNode) ->

replacedNode = oldNode.parentNode.replaceChild(newNode, oldNode)
triggerEvent('page:after-node-removed', replacedNode)

removeNode = (node) ->

removedNode = node.parentNode.removeChild(node)
triggerEvent('page:after-node-removed', removedNode)

# TODO: triggerEvent should be accessible to all these guys # on some kind of eventbus # TODO: clean up everything above me ^ # TODO: decide on the public API class window.Turbolinks

currentState = null
referer = null

fetch = (url, options = {}) ->
  return if pageChangePrevented(url)
  url = new ComponentUrl(url)

  rememberReferer()

  fetchReplacement(url, options)

isPartialReplace = (response, options) ->
  Boolean(
    options.partialReplace ||
    options.onlyKeys?.length ||
    options.exceptKeys?.length
  )

@fullPageNavigate: (url) ->
  if url?
    url = (new ComponentUrl(url)).absolute
    triggerEvent('page:before-full-refresh', url: url)
    activeDocument.location.href = url
  return

@pushState: (state, title, url) ->
  window.history.pushState(state, title, url)

@replaceState: (state, title, url) ->
  window.history.replaceState(state, title, url)

@document: (documentToUse) ->
  activeDocument = documentToUse if documentToUse
  activeDocument

fetchReplacement = (url, options) ->
  triggerEvent 'page:fetch', url: url.absolute

  if xhr?
    # Workaround for sinon xhr.abort()
    # https://github.com/sinonjs/sinon/issues/432#issuecomment-216917023
    xhr.readyState = 0
    xhr.statusText = "abort"
    xhr.abort()

  xhr = new XMLHttpRequest

  xhr.open 'GET', url.withoutHashForIE10compatibility(), true
  xhr.setRequestHeader 'Accept', 'text/html, application/xhtml+xml, application/xml'
  xhr.setRequestHeader 'X-XHR-Referer', referer
  options.headers ?= {}

  for k,v of options.headers
    xhr.setRequestHeader k, v

  xhr.onload = ->
    if xhr.status >= 500
      Turbolinks.fullPageNavigate(url)
    else
      Turbolinks.loadPage(url, xhr, options)
    xhr = null

  xhr.onerror = ->
    # Workaround for sinon xhr.abort()
    if xhr.statusText == "abort"
      xhr = null
      return
    Turbolinks.fullPageNavigate(url)

  xhr.send()

  return

@loadPage: (url, xhr, options = {}) ->
  triggerEvent 'page:receive'
  response = new Response(xhr, url)
  options.updatePushState ?= true
  options.partialReplace = isPartialReplace(response, options)

  unless upstreamDocument = response.document()
    triggerEvent 'page:error', xhr
    Turbolinks.fullPageNavigate(response.finalURL)
    return

  if options.partialReplace
    updateBody(upstreamDocument, response, options)
    return

  turbohead = new TurboHead(activeDocument, upstreamDocument)
  if turbohead.hasAssetConflicts()
    return Turbolinks.fullPageNavigate(response.finalURL)

  turbohead.waitForAssets().then((result) ->
    updateBody(upstreamDocument, response, options) unless result?.isCanceled
  )

updateBody = (upstreamDocument, response, options) ->
  nodes = changePage(
    upstreamDocument.querySelector('title')?.textContent,
    removeNoscriptTags(upstreamDocument.querySelector('body')),
    CSRFToken.get(upstreamDocument).token,
    'runScripts',
    options
  )
  reflectNewUrl(response.finalURL) if options.updatePushState

  Turbolinks.resetScrollPosition() unless options.partialReplace

  options.callback?()
  triggerEvent 'page:load', nodes

changePage = (title, body, csrfToken, runScripts, options = {}) ->
  activeDocument.title = title if title

  if options.onlyKeys?.length
    nodesToRefresh = [].concat(getNodesWithRefreshAlways(), getNodesMatchingRefreshKeys(options.onlyKeys))
    nodes = refreshNodes(nodesToRefresh, body)
    setAutofocusElement() if anyAutofocusElement(nodes)
    return nodes
  else
    refreshNodes(getNodesWithRefreshAlways(), body)
    persistStaticElements(body)
    if options.exceptKeys?.length
      refreshAllExceptWithKeys(options.exceptKeys, body)
    else
      deleteRefreshNeverNodes(body)

    triggerEvent 'page:before-replace'
    replaceNode(body, activeDocument.body)
    CSRFToken.update csrfToken if csrfToken?
    setAutofocusElement()
    executeScriptTags() if runScripts
    currentState = window.history.state
    triggerEvent 'page:change'
    triggerEvent 'page:update'

  return

getNodesMatchingRefreshKeys = (keys) ->
  matchingNodes = []
  for key in keys
    for node in TurboGraft.querySelectorAllTGAttribute(activeDocument, 'refresh', key)
      matchingNodes.push(node)

  return matchingNodes

getNodesWithRefreshAlways = ->
  matchingNodes = []
  for node in TurboGraft.querySelectorAllTGAttribute(activeDocument, 'refresh-always')
    matchingNodes.push(node)

  return matchingNodes

anyAutofocusElement = (nodes) ->
  for node in nodes
    if node.querySelectorAll('input[autofocus], textarea[autofocus]').length > 0
      return true

  false

setAutofocusElement = ->
  autofocusElement = (list = activeDocument.querySelectorAll 'input[autofocus], textarea[autofocus]')[list.length - 1]
  if autofocusElement and activeDocument.activeElement isnt autofocusElement
    autofocusElement.focus()

deleteRefreshNeverNodes = (body) ->
  for node in TurboGraft.querySelectorAllTGAttribute(body, 'refresh-never')
    removeNode(node)

  return

refreshNodes = (allNodesToBeRefreshed, body) ->
  triggerEvent 'page:before-partial-replace', allNodesToBeRefreshed

  parentIsRefreshing = (node) ->
    for potentialParent in allNodesToBeRefreshed when node != potentialParent
      return true if potentialParent.contains(node)
    false

  refreshedNodes = []
  for existingNode in allNodesToBeRefreshed
    continue if parentIsRefreshing(existingNode)

    unless nodeId = existingNode.getAttribute('id')
      throw new Error "Turbolinks refresh: Refresh key elements must have an id."

    if newNode = body.querySelector("##{ nodeId }")
      newNode = newNode.cloneNode(true)
      replaceNode(newNode, existingNode)

      if newNode.nodeName == 'SCRIPT' && newNode.dataset.turbolinksEval != "false"
        executeScriptTag(newNode)
      else
        refreshedNodes.push(newNode)

    else if !TurboGraft.hasTGAttribute(existingNode, "refresh-always")
      removeNode(existingNode)

  refreshedNodes

keepNodes = (body, allNodesToKeep) ->
  for existingNode in allNodesToKeep
    unless nodeId = existingNode.getAttribute('id')
      throw new Error("TurboGraft refresh: Kept nodes must have an id.")

    if remoteNode = body.querySelector("##{ nodeId }")
      replaceNode(existingNode, remoteNode)

persistStaticElements = (body) ->
  allNodesToKeep = []

  nodes = TurboGraft.querySelectorAllTGAttribute(activeDocument, 'tg-static')
  allNodesToKeep.push(node) for node in nodes

  keepNodes(body, allNodesToKeep)
  return

refreshAllExceptWithKeys = (keys, body) ->
  allNodesToKeep = []

  for key in keys
    for node in TurboGraft.querySelectorAllTGAttribute(activeDocument, 'refresh', key)
      allNodesToKeep.push(node)

  keepNodes(body, allNodesToKeep)
  return

executeScriptTags = ->
  scripts = Array::slice.call activeDocument.body.querySelectorAll 'script:not([data-turbolinks-eval="false"])'
  for script in scripts when script.type in ['', 'text/javascript']
    executeScriptTag(script)
  return

executeScriptTag = (script) ->
  copy = activeDocument.createElement 'script'
  copy.setAttribute attr.name, attr.value for attr in script.attributes
  copy.appendChild activeDocument.createTextNode script.innerHTML
  { parentNode, nextSibling } = script
  parentNode.removeChild script
  parentNode.insertBefore copy, nextSibling
  return

removeNoscriptTags = (node) ->
  node.innerHTML = node.innerHTML.replace /<noscript[\S\s]*?<\/noscript>/ig, ''
  node

reflectNewUrl = (url) ->
  if (url = new ComponentUrl url).absolute isnt referer
    Turbolinks.pushState { turbolinks: true, url: url.absolute }, '', url.absolute
  return

rememberReferer = ->
  referer = activeDocument.location.href

@rememberCurrentUrl: ->
  Turbolinks.replaceState { turbolinks: true, url: activeDocument.location.href }, '', activeDocument.location.href

@rememberCurrentState: ->
  currentState = window.history.state

recallScrollPosition = (page) ->
  window.scrollTo page.positionX, page.positionY

@resetScrollPosition: ->
  if activeDocument.location.hash
    activeDocument.location.href = activeDocument.location.href
  else
    window.scrollTo 0, 0

pageChangePrevented = (url) ->
  !triggerEvent('page:before-change', url)

installHistoryChangeHandler = (event) ->
  if event.state?.turbolinks
    Turbolinks.visit event.target.location.href

# Delay execution of function long enough to miss the popstate event
# some browsers fire on the initial page load.
bypassOnLoadPopstate = (fn) ->
  setTimeout fn, 500

if browserSupportsTurbolinks
  @visit = fetch
  @rememberCurrentUrl()
  @rememberCurrentState()

  activeDocument.addEventListener 'click', Click.installHandlerLast, true

  bypassOnLoadPopstate ->
    window.addEventListener 'popstate', installHistoryChangeHandler, false

else
  @visit = (url) -> activeDocument.location.href = url