const _ = require('underscore-plus')
const {CompositeDisposable, Emitter} = require('event-kit')
const {Point, Range} = require('text-buffer')
const TokenizedLine = require('./tokenized-line')
const TokenIterator = require('./token-iterator')
const ScopeDescriptor = require('./scope-descriptor')
const NullGrammar = require('./null-grammar')
const {OnigRegExp} = require('oniguruma')
const {toFirstMateScopeId, fromFirstMateScopeId} = require('./first-mate-helpers')

const NON_WHITESPACE_REGEX = /\S/

let nextId = 0
const prefixedScopes = new Map()

class TextMateLanguageMode {
  constructor (params) {
    this.emitter = new Emitter()
    this.disposables = new CompositeDisposable()
    this.tokenIterator = new TokenIterator(this)
    this.regexesByPattern = {}

    this.alive = true
    this.tokenizationStarted = false
    this.id = params.id != null ? params.id : nextId++
    this.buffer = params.buffer
    this.largeFileMode = params.largeFileMode
    this.config = params.config
    this.largeFileMode = params.largeFileMode != null
      ? params.largeFileMode
      : this.buffer.buffer.getLength() >= 2 * 1024 * 1024

    this.grammar = params.grammar || NullGrammar
    this.rootScopeDescriptor = new ScopeDescriptor({scopes: [this.grammar.scopeName]})
    this.disposables.add(this.grammar.onDidUpdate(() => this.retokenizeLines()))
    this.retokenizeLines()
  }

  destroy () {
    if (!this.alive) return
    this.alive = false
    this.disposables.dispose()
    this.tokenizedLines.length = 0
  }

  isAlive () {
    return this.alive
  }

  isDestroyed () {
    return !this.alive
  }

  getGrammar () {
    return this.grammar
  }

  getLanguageId () {
    return this.grammar.scopeName
  }

  getNonWordCharacters (position) {
    const scope = this.scopeDescriptorForPosition(position)
    return this.config.get('editor.nonWordCharacters', {scope})
  }

  /*
  Section - auto-indent
  */

  // Get the suggested indentation level for an existing line in the buffer.
  //
  // * bufferRow - A {Number} indicating the buffer row
  //
  // Returns a {Number}.
  suggestedIndentForBufferRow (bufferRow, tabLength, options) {
    const line = this.buffer.lineForRow(bufferRow)
    const tokenizedLine = this.tokenizedLineForRow(bufferRow)
    const iterator = tokenizedLine.getTokenIterator()
    iterator.next()
    const scopeDescriptor = new ScopeDescriptor({scopes: iterator.getScopes()})
    return this._suggestedIndentForLineWithScopeAtBufferRow(
      bufferRow,
      line,
      scopeDescriptor,
      tabLength,
      options
    )
  }

  // Get the suggested indentation level for a given line of text, if it were inserted at the given
  // row in the buffer.
  //
  // * bufferRow - A {Number} indicating the buffer row
  //
  // Returns a {Number}.
  suggestedIndentForLineAtBufferRow (bufferRow, line, tabLength) {
    const tokenizedLine = this.buildTokenizedLineForRowWithText(bufferRow, line)
    const iterator = tokenizedLine.getTokenIterator()
    iterator.next()
    const scopeDescriptor = new ScopeDescriptor({scopes: iterator.getScopes()})
    return this._suggestedIndentForLineWithScopeAtBufferRow(
      bufferRow,
      line,
      scopeDescriptor,
      tabLength
    )
  }

  // Get the suggested indentation level for a line in the buffer on which the user is currently
  // typing. This may return a different result from {::suggestedIndentForBufferRow} in order
  // to avoid unexpected changes in indentation. It may also return undefined if no change should
  // be made.
  //
  // * bufferRow - The row {Number}
  //
  // Returns a {Number}.
  suggestedIndentForEditedBufferRow (bufferRow, tabLength) {
    const line = this.buffer.lineForRow(bufferRow)
    const currentIndentLevel = this.indentLevelForLine(line, tabLength)
    if (currentIndentLevel === 0) return

    const scopeDescriptor = this.scopeDescriptorForPosition(new Point(bufferRow, 0))
    const decreaseIndentRegex = this.decreaseIndentRegexForScopeDescriptor(scopeDescriptor)
    if (!decreaseIndentRegex) return

    if (!decreaseIndentRegex.testSync(line)) return

    const precedingRow = this.buffer.previousNonBlankRow(bufferRow)
    if (precedingRow == null) return

    const precedingLine = this.buffer.lineForRow(precedingRow)
    let desiredIndentLevel = this.indentLevelForLine(precedingLine, tabLength)

    const increaseIndentRegex = this.increaseIndentRegexForScopeDescriptor(scopeDescriptor)
    if (increaseIndentRegex) {
      if (!increaseIndentRegex.testSync(precedingLine)) desiredIndentLevel -= 1
    }

    const decreaseNextIndentRegex = this.decreaseNextIndentRegexForScopeDescriptor(scopeDescriptor)
    if (decreaseNextIndentRegex) {
      if (decreaseNextIndentRegex.testSync(precedingLine)) desiredIndentLevel -= 1
    }

    if (desiredIndentLevel < 0) return 0
    if (desiredIndentLevel >= currentIndentLevel) return
    return desiredIndentLevel
  }

  _suggestedIndentForLineWithScopeAtBufferRow (bufferRow, line, scopeDescriptor, tabLength, options) {
    const increaseIndentRegex = this.increaseIndentRegexForScopeDescriptor(scopeDescriptor)
    const decreaseIndentRegex = this.decreaseIndentRegexForScopeDescriptor(scopeDescriptor)
    const decreaseNextIndentRegex = this.decreaseNextIndentRegexForScopeDescriptor(scopeDescriptor)

    let precedingRow
    if (!options || options.skipBlankLines !== false) {
      precedingRow = this.buffer.previousNonBlankRow(bufferRow)
      if (precedingRow == null) return 0
    } else {
      precedingRow = bufferRow - 1
      if (precedingRow < 0) return 0
    }

    const precedingLine = this.buffer.lineForRow(precedingRow)
    let desiredIndentLevel = this.indentLevelForLine(precedingLine, tabLength)
    if (!increaseIndentRegex) return desiredIndentLevel

    if (!this.isRowCommented(precedingRow)) {
      if (increaseIndentRegex && increaseIndentRegex.testSync(precedingLine)) desiredIndentLevel += 1
      if (decreaseNextIndentRegex && decreaseNextIndentRegex.testSync(precedingLine)) desiredIndentLevel -= 1
    }

    if (!this.buffer.isRowBlank(precedingRow)) {
      if (decreaseIndentRegex && decreaseIndentRegex.testSync(line)) desiredIndentLevel -= 1
    }

    return Math.max(desiredIndentLevel, 0)
  }

  /*
  Section - Comments
  */

  commentStringsForPosition (position) {
    const scope = this.scopeDescriptorForPosition(position)
    const commentStartEntries = this.config.getAll('editor.commentStart', {scope})
    const commentEndEntries = this.config.getAll('editor.commentEnd', {scope})
    const commentStartEntry = commentStartEntries[0]
    const commentEndEntry = commentEndEntries.find((entry) => {
      return entry.scopeSelector === commentStartEntry.scopeSelector
    })
    return {
      commentStartString: commentStartEntry && commentStartEntry.value,
      commentEndString: commentEndEntry && commentEndEntry.value
    }
  }

  /*
  Section - Syntax Highlighting
  */

  buildHighlightIterator () {
    return new TextMateHighlightIterator(this)
  }

  classNameForScopeId (id) {
    const scope = this.grammar.scopeForId(toFirstMateScopeId(id))
    if (scope) {
      let prefixedScope = prefixedScopes.get(scope)
      if (prefixedScope) {
        return prefixedScope
      } else {
        prefixedScope = `syntax--${scope.replace(/\./g, ' syntax--')}`
        prefixedScopes.set(scope, prefixedScope)
        return prefixedScope
      }
    } else {
      return null
    }
  }

  getInvalidatedRanges () {
    return []
  }

  onDidChangeHighlighting (fn) {
    return this.emitter.on('did-change-highlighting', fn)
  }

  onDidTokenize (callback) {
    return this.emitter.on('did-tokenize', callback)
  }

  getGrammarSelectionContent () {
    return this.buffer.getTextInRange([[0, 0], [10, 0]])
  }

  hasTokenForSelector (selector) {
    for (const tokenizedLine of this.tokenizedLines) {
      if (tokenizedLine) {
        for (let token of tokenizedLine.tokens) {
          if (selector.matches(token.scopes)) return true
        }
      }
    }
    return false
  }

  retokenizeLines () {
    if (!this.alive) return
    this.fullyTokenized = false
    this.tokenizedLines = new Array(this.buffer.getLineCount())
    this.invalidRows = []
    if (this.largeFileMode || this.grammar.name === 'Null Grammar') {
      this.markTokenizationComplete()
    } else {
      this.invalidateRow(0)
    }
  }

  startTokenizing () {
    this.tokenizationStarted = true
    if (this.grammar.name !== 'Null Grammar' && !this.largeFileMode) {
      this.tokenizeInBackground()
    }
  }

  tokenizeInBackground () {
    if (!this.tokenizationStarted || this.pendingChunk || !this.alive) return

    this.pendingChunk = true
    _.defer(() => {
      this.pendingChunk = false
      if (this.isAlive() && this.buffer.isAlive()) this.tokenizeNextChunk()
    })
  }

  tokenizeNextChunk () {
    let rowsRemaining = this.chunkSize

    while (this.firstInvalidRow() != null && rowsRemaining > 0) {
      var endRow, filledRegion
      const startRow = this.invalidRows.shift()
      const lastRow = this.buffer.getLastRow()
      if (startRow > lastRow) continue

      let row = startRow
      while (true) {
        const previousStack = this.stackForRow(row)
        this.tokenizedLines[row] = this.buildTokenizedLineForRow(row, this.stackForRow(row - 1), this.openScopesForRow(row))
        if (--rowsRemaining === 0) {
          filledRegion = false
          endRow = row
          break
        }
        if (row === lastRow || _.isEqual(this.stackForRow(row), previousStack)) {
          filledRegion = true
          endRow = row
          break
        }
        row++
      }

      this.validateRow(endRow)
      if (!filledRegion) this.invalidateRow(endRow + 1)

      this.emitter.emit('did-change-highlighting', Range(Point(startRow, 0), Point(endRow + 1, 0)))
    }

    if (this.firstInvalidRow() != null) {
      this.tokenizeInBackground()
    } else {
      this.markTokenizationComplete()
    }
  }

  markTokenizationComplete () {
    if (!this.fullyTokenized) {
      this.emitter.emit('did-tokenize')
    }
    this.fullyTokenized = true
  }

  firstInvalidRow () {
    return this.invalidRows[0]
  }

  validateRow (row) {
    while (this.invalidRows[0] <= row) this.invalidRows.shift()
  }

  invalidateRow (row) {
    this.invalidRows.push(row)
    this.invalidRows.sort((a, b) => a - b)
    this.tokenizeInBackground()
  }

  updateInvalidRows (start, end, delta) {
    this.invalidRows = this.invalidRows.map((row) => {
      if (row < start) {
        return row
      } else if (start <= row && row <= end) {
        return end + delta + 1
      } else if (row > end) {
        return row + delta
      }
    })
  }

  bufferDidChange (e) {
    this.changeCount = this.buffer.changeCount

    const {oldRange, newRange} = e
    const start = oldRange.start.row
    const end = oldRange.end.row
    const delta = newRange.end.row - oldRange.end.row
    const oldLineCount = (oldRange.end.row - oldRange.start.row) + 1
    const newLineCount = (newRange.end.row - newRange.start.row) + 1

    this.updateInvalidRows(start, end, delta)
    const previousEndStack = this.stackForRow(end) // used in spill detection below
    if (this.largeFileMode || (this.grammar.name === 'Null Grammar')) {
      _.spliceWithArray(this.tokenizedLines, start, oldLineCount, new Array(newLineCount))
    } else {
      const newTokenizedLines = this.buildTokenizedLinesForRows(start, end + delta, this.stackForRow(start - 1), this.openScopesForRow(start))
      _.spliceWithArray(this.tokenizedLines, start, oldLineCount, newTokenizedLines)
      const newEndStack = this.stackForRow(end + delta)
      if (newEndStack && !_.isEqual(newEndStack, previousEndStack)) {
        this.invalidateRow(end + delta + 1)
      }
    }
  }

  isFoldableAtRow (row) {
    return this.endRowForFoldAtRow(row, 1, true) != null
  }

  buildTokenizedLinesForRows (startRow, endRow, startingStack, startingopenScopes) {
    let ruleStack = startingStack
    let openScopes = startingopenScopes
    const stopTokenizingAt = startRow + this.chunkSize
    const tokenizedLines = []
    for (let row = startRow, end = endRow; row <= end; row++) {
      let tokenizedLine
      if ((ruleStack || (row === 0)) && row < stopTokenizingAt) {
        tokenizedLine = this.buildTokenizedLineForRow(row, ruleStack, openScopes)
        ruleStack = tokenizedLine.ruleStack
        openScopes = this.scopesFromTags(openScopes, tokenizedLine.tags)
      }
      tokenizedLines.push(tokenizedLine)
    }

    if (endRow >= stopTokenizingAt) {
      this.invalidateRow(stopTokenizingAt)
      this.tokenizeInBackground()
    }

    return tokenizedLines
  }

  buildTokenizedLineForRow (row, ruleStack, openScopes) {
    return this.buildTokenizedLineForRowWithText(row, this.buffer.lineForRow(row), ruleStack, openScopes)
  }

  buildTokenizedLineForRowWithText (row, text, currentRuleStack = this.stackForRow(row - 1), openScopes = this.openScopesForRow(row)) {
    const lineEnding = this.buffer.lineEndingForRow(row)
    const {tags, ruleStack} = this.grammar.tokenizeLine(text, currentRuleStack, row === 0, false)
    return new TokenizedLine({
      openScopes,
      text,
      tags,
      ruleStack,
      lineEnding,
      tokenIterator: this.tokenIterator,
      grammar: this.grammar
    })
  }

  tokenizedLineForRow (bufferRow) {
    if (bufferRow >= 0 && bufferRow <= this.buffer.getLastRow()) {
      const tokenizedLine = this.tokenizedLines[bufferRow]
      if (tokenizedLine) {
        return tokenizedLine
      } else {
        const text = this.buffer.lineForRow(bufferRow)
        const lineEnding = this.buffer.lineEndingForRow(bufferRow)
        const tags = [
          this.grammar.startIdForScope(this.grammar.scopeName),
          text.length,
          this.grammar.endIdForScope(this.grammar.scopeName)
        ]
        this.tokenizedLines[bufferRow] = new TokenizedLine({
          openScopes: [],
          text,
          tags,
          lineEnding,
          tokenIterator: this.tokenIterator,
          grammar: this.grammar
        })
        return this.tokenizedLines[bufferRow]
      }
    }
  }

  tokenizedLinesForRows (startRow, endRow) {
    const result = []
    for (let row = startRow, end = endRow; row <= end; row++) {
      result.push(this.tokenizedLineForRow(row))
    }
    return result
  }

  stackForRow (bufferRow) {
    return this.tokenizedLines[bufferRow] && this.tokenizedLines[bufferRow].ruleStack
  }

  openScopesForRow (bufferRow) {
    const precedingLine = this.tokenizedLines[bufferRow - 1]
    if (precedingLine) {
      return this.scopesFromTags(precedingLine.openScopes, precedingLine.tags)
    } else {
      return []
    }
  }

  scopesFromTags (startingScopes, tags) {
    const scopes = startingScopes.slice()
    for (const tag of tags) {
      if (tag < 0) {
        if (tag % 2 === -1) {
          scopes.push(tag)
        } else {
          const matchingStartTag = tag + 1
          while (true) {
            if (scopes.pop() === matchingStartTag) break
            if (scopes.length === 0) {
              break
            }
          }
        }
      }
    }
    return scopes
  }

  indentLevelForLine (line, tabLength) {
    let indentLength = 0
    for (let i = 0, {length} = line; i < length; i++) {
      const char = line[i]
      if (char === '\t') {
        indentLength += tabLength - (indentLength % tabLength)
      } else if (char === ' ') {
        indentLength++
      } else {
        break
      }
    }
    return indentLength / tabLength
  }

  scopeDescriptorForPosition (position) {
    let scopes
    const {row, column} = this.buffer.clipPosition(Point.fromObject(position))

    const iterator = this.tokenizedLineForRow(row).getTokenIterator()
    while (iterator.next()) {
      if (iterator.getBufferEnd() > column) {
        scopes = iterator.getScopes()
        break
      }
    }

    // rebuild scope of last token if we iterated off the end
    if (!scopes) {
      scopes = iterator.getScopes()
      scopes.push(...iterator.getScopeEnds().reverse())
    }

    return new ScopeDescriptor({scopes})
  }

  tokenForPosition (position) {
    const {row, column} = Point.fromObject(position)
    return this.tokenizedLineForRow(row).tokenAtBufferColumn(column)
  }

  tokenStartPositionForPosition (position) {
    let {row, column} = Point.fromObject(position)
    column = this.tokenizedLineForRow(row).tokenStartColumnForBufferColumn(column)
    return new Point(row, column)
  }

  bufferRangeForScopeAtPosition (selector, position) {
    let endColumn, tag, tokenIndex
    position = Point.fromObject(position)

    const {openScopes, tags} = this.tokenizedLineForRow(position.row)
    const scopes = openScopes.map(tag => this.grammar.scopeForId(tag))

    let startColumn = 0
    for (tokenIndex = 0; tokenIndex < tags.length; tokenIndex++) {
      tag = tags[tokenIndex]
      if (tag < 0) {
        if ((tag % 2) === -1) {
          scopes.push(this.grammar.scopeForId(tag))
        } else {
          scopes.pop()
        }
      } else {
        endColumn = startColumn + tag
        if (endColumn >= position.column) {
          break
        } else {
          startColumn = endColumn
        }
      }
    }

    if (!selectorMatchesAnyScope(selector, scopes)) return

    const startScopes = scopes.slice()
    for (let startTokenIndex = tokenIndex - 1; startTokenIndex >= 0; startTokenIndex--) {
      tag = tags[startTokenIndex]
      if (tag < 0) {
        if ((tag % 2) === -1) {
          startScopes.pop()
        } else {
          startScopes.push(this.grammar.scopeForId(tag))
        }
      } else {
        if (!selectorMatchesAnyScope(selector, startScopes)) { break }
        startColumn -= tag
      }
    }

    const endScopes = scopes.slice()
    for (let endTokenIndex = tokenIndex + 1, end = tags.length; endTokenIndex < end; endTokenIndex++) {
      tag = tags[endTokenIndex]
      if (tag < 0) {
        if ((tag % 2) === -1) {
          endScopes.push(this.grammar.scopeForId(tag))
        } else {
          endScopes.pop()
        }
      } else {
        if (!selectorMatchesAnyScope(selector, endScopes)) { break }
        endColumn += tag
      }
    }

    return new Range(new Point(position.row, startColumn), new Point(position.row, endColumn))
  }

  isRowCommented (row) {
    return this.tokenizedLines[row] && this.tokenizedLines[row].isComment()
  }

  getFoldableRangeContainingPoint (point, tabLength) {
    if (point.column >= this.buffer.lineLengthForRow(point.row)) {
      const endRow = this.endRowForFoldAtRow(point.row, tabLength)
      if (endRow != null) {
        return Range(Point(point.row, Infinity), Point(endRow, Infinity))
      }
    }

    for (let row = point.row - 1; row >= 0; row--) {
      const endRow = this.endRowForFoldAtRow(row, tabLength)
      if (endRow != null && endRow >= point.row) {
        return Range(Point(row, Infinity), Point(endRow, Infinity))
      }
    }
    return null
  }

  getFoldableRangesAtIndentLevel (indentLevel, tabLength) {
    const result = []
    let row = 0
    const lineCount = this.buffer.getLineCount()
    while (row < lineCount) {
      if (this.indentLevelForLine(this.buffer.lineForRow(row), tabLength) === indentLevel) {
        const endRow = this.endRowForFoldAtRow(row, tabLength)
        if (endRow != null) {
          result.push(Range(Point(row, Infinity), Point(endRow, Infinity)))
          row = endRow + 1
          continue
        }
      }
      row++
    }
    return result
  }

  getFoldableRanges (tabLength) {
    const result = []
    let row = 0
    const lineCount = this.buffer.getLineCount()
    while (row < lineCount) {
      const endRow = this.endRowForFoldAtRow(row, tabLength)
      if (endRow != null) {
        result.push(Range(Point(row, Infinity), Point(endRow, Infinity)))
      }
      row++
    }
    return result
  }

  endRowForFoldAtRow (row, tabLength, existenceOnly = false) {
    if (this.isRowCommented(row)) {
      return this.endRowForCommentFoldAtRow(row, existenceOnly)
    } else {
      return this.endRowForCodeFoldAtRow(row, tabLength, existenceOnly)
    }
  }

  endRowForCommentFoldAtRow (row, existenceOnly) {
    if (this.isRowCommented(row - 1)) return

    let endRow
    for (let nextRow = row + 1, end = this.buffer.getLineCount(); nextRow < end; nextRow++) {
      if (!this.isRowCommented(nextRow)) break
      endRow = nextRow
      if (existenceOnly) break
    }

    return endRow
  }

  endRowForCodeFoldAtRow (row, tabLength, existenceOnly) {
    let foldEndRow
    const line = this.buffer.lineForRow(row)
    if (!NON_WHITESPACE_REGEX.test(line)) return
    const startIndentLevel = this.indentLevelForLine(line, tabLength)
    const scopeDescriptor = this.scopeDescriptorForPosition([row, 0])
    const foldEndRegex = this.foldEndRegexForScopeDescriptor(scopeDescriptor)
    for (let nextRow = row + 1, end = this.buffer.getLineCount(); nextRow < end; nextRow++) {
      const line = this.buffer.lineForRow(nextRow)
      if (!NON_WHITESPACE_REGEX.test(line)) continue
      const indentation = this.indentLevelForLine(line, tabLength)
      if (indentation < startIndentLevel) {
        break
      } else if (indentation === startIndentLevel) {
        if (foldEndRegex && foldEndRegex.searchSync(line)) foldEndRow = nextRow
        break
      }
      foldEndRow = nextRow
      if (existenceOnly) break
    }
    return foldEndRow
  }

  increaseIndentRegexForScopeDescriptor (scope) {
    return this.regexForPattern(this.config.get('editor.increaseIndentPattern', {scope}))
  }

  decreaseIndentRegexForScopeDescriptor (scope) {
    return this.regexForPattern(this.config.get('editor.decreaseIndentPattern', {scope}))
  }

  decreaseNextIndentRegexForScopeDescriptor (scope) {
    return this.regexForPattern(this.config.get('editor.decreaseNextIndentPattern', {scope}))
  }

  foldEndRegexForScopeDescriptor (scope) {
    return this.regexForPattern(this.config.get('editor.foldEndPattern', {scope}))
  }

  regexForPattern (pattern) {
    if (pattern) {
      if (!this.regexesByPattern[pattern]) {
        this.regexesByPattern[pattern] = new OnigRegExp(pattern)
      }
      return this.regexesByPattern[pattern]
    }
  }

  logLines (start = 0, end = this.buffer.getLastRow()) {
    for (let row = start; row <= end; row++) {
      const line = this.tokenizedLines[row].text
      console.log(row, line, line.length)
    }
  }
}

TextMateLanguageMode.prototype.chunkSize = 50

function selectorMatchesAnyScope (selector, scopes) {
  const targetClasses = selector.replace(/^\./, '').split('.')
  return scopes.some((scope) => {
    const scopeClasses = scope.split('.')
    return _.isSubset(targetClasses, scopeClasses)
  })
}

class TextMateHighlightIterator {
  constructor (languageMode) {
    this.languageMode = languageMode
    this.openScopeIds = null
    this.closeScopeIds = null
  }

  seek (position) {
    this.openScopeIds = []
    this.closeScopeIds = []
    this.tagIndex = null

    const currentLine = this.languageMode.tokenizedLineForRow(position.row)
    this.currentLineTags = currentLine.tags
    this.currentLineLength = currentLine.text.length
    const containingScopeIds = currentLine.openScopes.map((id) => fromFirstMateScopeId(id))

    let currentColumn = 0
    for (let index = 0; index < this.currentLineTags.length; index++) {
      const tag = this.currentLineTags[index]
      if (tag >= 0) {
        if (currentColumn >= position.column) {
          this.tagIndex = index
          break
        } else {
          currentColumn += tag
          while (this.closeScopeIds.length > 0) {
            this.closeScopeIds.shift()
            containingScopeIds.pop()
          }
          while (this.openScopeIds.length > 0) {
            const openTag = this.openScopeIds.shift()
            containingScopeIds.push(openTag)
          }
        }
      } else {
        const scopeId = fromFirstMateScopeId(tag)
        if ((tag & 1) === 0) {
          if (this.openScopeIds.length > 0) {
            if (currentColumn >= position.column) {
              this.tagIndex = index
              break
            } else {
              while (this.closeScopeIds.length > 0) {
                this.closeScopeIds.shift()
                containingScopeIds.pop()
              }
              while (this.openScopeIds.length > 0) {
                const openTag = this.openScopeIds.shift()
                containingScopeIds.push(openTag)
              }
            }
          }
          this.closeScopeIds.push(scopeId)
        } else {
          this.openScopeIds.push(scopeId)
        }
      }
    }

    if (this.tagIndex == null) {
      this.tagIndex = this.currentLineTags.length
    }
    this.position = Point(position.row, Math.min(this.currentLineLength, currentColumn))
    return containingScopeIds
  }

  moveToSuccessor () {
    this.openScopeIds = []
    this.closeScopeIds = []
    while (true) {
      if (this.tagIndex === this.currentLineTags.length) {
        if (this.isAtTagBoundary()) {
          break
        } else if (!this.moveToNextLine()) {
          return false
        }
      } else {
        const tag = this.currentLineTags[this.tagIndex]
        if (tag >= 0) {
          if (this.isAtTagBoundary()) {
            break
          } else {
            this.position = Point(this.position.row, Math.min(
              this.currentLineLength,
              this.position.column + this.currentLineTags[this.tagIndex]
            ))
          }
        } else {
          const scopeId = fromFirstMateScopeId(tag)
          if ((tag & 1) === 0) {
            if (this.openScopeIds.length > 0) {
              break
            } else {
              this.closeScopeIds.push(scopeId)
            }
          } else {
            this.openScopeIds.push(scopeId)
          }
        }
        this.tagIndex++
      }
    }
    return true
  }

  getPosition () {
    return this.position
  }

  getCloseScopeIds () {
    return this.closeScopeIds.slice()
  }

  getOpenScopeIds () {
    return this.openScopeIds.slice()
  }

  moveToNextLine () {
    this.position = Point(this.position.row + 1, 0)
    const tokenizedLine = this.languageMode.tokenizedLineForRow(this.position.row)
    if (tokenizedLine == null) {
      return false
    } else {
      this.currentLineTags = tokenizedLine.tags
      this.currentLineLength = tokenizedLine.text.length
      this.tagIndex = 0
      return true
    }
  }

  isAtTagBoundary () {
    return this.closeScopeIds.length > 0 || this.openScopeIds.length > 0
  }
}

TextMateLanguageMode.TextMateHighlightIterator = TextMateHighlightIterator
module.exports = TextMateLanguageMode
