import OError from '@overleaf/o-error'
import { Text } from '@codemirror/state'
import { Word } from './spellchecker'
import { ViewUpdate } from '@codemirror/view'

export class LineTracker {
  private _lines: boolean[]
  constructor(doc: Text) {
    /*
     * Maintain an array of booleans, one for each line of the document.
     * `true` means a line has changed
     */
    this._lines = new Array(doc.lines).fill(true)
  }

  dump() {
    return [...this._lines]
  }

  count() {
    return this._lines.length
  }

  lineHasChanged(lineNumber: number) {
    return this._lines[lineNumber - 1] === true
  }

  /*
   * Given a list of words, clear the 'changed' mark
   * on the lines the words are on
   */
  clearChangedLinesForWords(words: Word[]) {
    words.forEach(word => {
      this.clearLine(word.lineNumber)
    })
  }

  clearLine(lineNumber: number) {
    this._lines[lineNumber - 1] = false
  }

  clearAllLines() {
    this._lines = this._lines.map(() => false)
  }

  resetAllLines() {
    this._lines = this._lines.map(() => true)
  }

  markLineAsUpdated(lineNumber: number) {
    this._lines[lineNumber - 1] = true
  }

  /*
   * On update, for all changes, mark the affected lines
   * as changed
   */
  applyUpdate(update: ViewUpdate) {
    for (const transaction of update.transactions) {
      if (transaction.docChanged) {
        let positionShift = 0
        transaction.changes.iterChanges(
          (fromA, toA, fromB, toB, insertedText) => {
            const insertedLength = insertedText.length
            const removedLength = toA - fromA
            const hasInserted = insertedLength > 0
            const hasRemoved = removedLength > 0
            const doc = update.state.doc
            const oldDoc = transaction.startState.doc
            if (hasRemoved) {
              const startLine = oldDoc.lineAt(fromA).number
              const endLine = oldDoc.lineAt(toA).number
              /* Mark start line as changed, and remove deleted lines
               * Example:
               *   with this text:
               *   |1|aaaa|
               *   |2|bbbb|  => [false, false, false, false]
               *   |3|cccc|
               *   |4|dddd|
               *
               *   with a selection covering 'bbcccc' across lines 2 and 3,
               *   press backspace,
               *   resulting in:
               *   |1|aaaa|
               *   |2|bb|  => [false, true, false]
               *   |3|dddd|
               */
              this._lines.splice(startLine - 1, endLine - startLine + 1, true)
            }
            if (hasInserted) {
              const startLine = doc.lineAt(fromB).number
              /* Mark start line as changed, and insert new (changed) lines after.
               * Example:
               *   with this text:
               *   |1|aaaa|
               *   |2|bbbb|  => [false, false, false]
               *   |3|cccc|
               *
               *   with the cursor at the end of line 2,
               *   insert the following text:
               *   |1|xx|
               *   |2|yy|
               *
               *   results in:
               *   |1|aaaa|
               *   |2|bbbbxx|  => [false, true, true, false]
               *   |3|yy|
               *   |4|cccc|
               */
              const changes = new Array(insertedText.lines).fill(true)
              this._lines.splice(startLine - 1, 1, ...changes)
            }
            positionShift = positionShift - removedLength + insertedLength
          }
        )
        if (update.state.doc.lines !== this._lines.length) {
          throw new OError(
            'LineTracker length does not match document line count'
          ).withInfo({
            documentLines: update.state.doc.lines,
            trackerLines: this._lines.length,
          })
        }
      }
    }
  }
}
