{elementContainsNode, findChildIndexOfNode, nodeIsBlockStart,
 nodeIsBlockStartComment, nodeIsBlockContainer, nodeIsCursorTarget,
 nodeIsEmptyTextNode, nodeIsTextNode, nodeIsAttachmentElement, tagName, walkTree} = Trix

class Trix.LocationMapper
  constructor: (@element) ->

  findLocationFromContainerAndOffset: (container, offset, {strict} = strict: true) ->
    childIndex = 0
    foundBlock = false
    location = index: 0, offset: 0

    if attachmentElement = @findAttachmentElementParentForNode(container)
      container = attachmentElement.parentNode
      offset = findChildIndexOfNode(attachmentElement)

    walker = walkTree(@element, usingFilter: rejectAttachmentContents)

    while walker.nextNode()
      node = walker.currentNode

      if node is container and nodeIsTextNode(container)
        unless nodeIsCursorTarget(node)
          location.offset += offset
        break

      else
        if node.parentNode is container
          break if childIndex++ is offset
        else unless elementContainsNode(container, node)
          break if childIndex > 0

        if nodeIsBlockStart(node, {strict})
          location.index++ if foundBlock
          location.offset = 0
          foundBlock = true
        else
          location.offset += nodeLength(node)

    location

  findContainerAndOffsetFromLocation: (location) ->
    if location.index is 0 and location.offset is 0
      container = @element
      offset = 0

      while container.firstChild
        container = container.firstChild
        if nodeIsBlockContainer(container)
          offset = 1
          break

      return [container, offset]

    [node, nodeOffset] = @findNodeAndOffsetFromLocation(location)
    return unless node

    if nodeIsTextNode(node)
      container = node
      string = node.textContent
      offset = location.offset - nodeOffset

    else
      container = node.parentNode

      unless nodeIsBlockStart(node.previousSibling)
        unless nodeIsBlockContainer(container)
          while node is container.lastChild
            node = container
            container = container.parentNode
            break if nodeIsBlockContainer(container)

      offset = findChildIndexOfNode(node)
      offset++ unless location.offset is 0

    [container, offset]

  findNodeAndOffsetFromLocation: (location) ->
    offset = 0

    for currentNode in @getSignificantNodesForIndex(location.index)
      length = nodeLength(currentNode)

      if location.offset <= offset + length
        if nodeIsTextNode(currentNode)
          node = currentNode
          nodeOffset = offset
          break if location.offset is nodeOffset and nodeIsCursorTarget(node)

        else if not node
          node = currentNode
          nodeOffset = offset

      offset += length
      break if offset > location.offset

    [node, nodeOffset]

  # Private

  findAttachmentElementParentForNode: (node) ->
    while node and node isnt @element
      return node if nodeIsAttachmentElement(node)
      node = node.parentNode

  getSignificantNodesForIndex: (index) ->
    nodes = []
    walker = walkTree(@element, usingFilter: acceptSignificantNodes)
    recordingNodes = false

    while walker.nextNode()
      node = walker.currentNode
      if nodeIsBlockStartComment(node)
        if blockIndex?
          blockIndex++
        else
          blockIndex = 0

        if blockIndex is index
          recordingNodes = true
        else if recordingNodes
          break
      else if recordingNodes
        nodes.push(node)

    nodes

  nodeLength = (node) ->
    if node.nodeType is Node.TEXT_NODE
      if nodeIsCursorTarget(node)
        0
      else
        string = node.textContent
        string.length
    else if tagName(node) is "br" or nodeIsAttachmentElement(node)
      1
    else
      0

  acceptSignificantNodes = (node) ->
    if rejectEmptyTextNodes(node) is NodeFilter.FILTER_ACCEPT
      rejectAttachmentContents(node)
    else
      NodeFilter.FILTER_REJECT

  rejectEmptyTextNodes = (node) ->
    if nodeIsEmptyTextNode(node)
      NodeFilter.FILTER_REJECT
    else
      NodeFilter.FILTER_ACCEPT

  rejectAttachmentContents = (node) ->
    if nodeIsAttachmentElement(node.parentNode)
      NodeFilter.FILTER_REJECT
    else
      NodeFilter.FILTER_ACCEPT