import {Selection, NodeSelection} from "prosemirror-state"
import {Slice, ResolvedPos, Node} from "prosemirror-model"
import {Mappable} from "prosemirror-transform"

/// Gap cursor selections are represented using this class. Its
/// `$anchor` and `$head` properties both point at the cursor position.
export class GapCursor extends Selection {
  /// Create a gap cursor.
  constructor($pos: ResolvedPos) {
    super($pos, $pos)
  }

  map(doc: Node, mapping: Mappable): Selection {
    let $pos = doc.resolve(mapping.map(this.head))
    return GapCursor.valid($pos) ? new GapCursor($pos) : Selection.near($pos)
  }

  content() { return Slice.empty }

  eq(other: Selection): boolean {
    return other instanceof GapCursor && other.head == this.head
  }

  toJSON(): any {
    return {type: "gapcursor", pos: this.head}
  }

  /// @internal
  static fromJSON(doc: Node, json: any): GapCursor {
    if (typeof json.pos != "number") throw new RangeError("Invalid input for GapCursor.fromJSON")
    return new GapCursor(doc.resolve(json.pos))
  }

  /// @internal
  getBookmark() { return new GapBookmark(this.anchor) }

  /// @internal
  static valid($pos: ResolvedPos) {
    let parent = $pos.parent
    if (parent.isTextblock || !closedBefore($pos) || !closedAfter($pos)) return false
    let override = parent.type.spec.allowGapCursor
    if (override != null) return override
    let deflt = parent.contentMatchAt($pos.index()).defaultType
    return deflt && deflt.isTextblock
  }

  /// @internal
  static findGapCursorFrom($pos: ResolvedPos, dir: number, mustMove = false) {
    search: for (;;) {
      if (!mustMove && GapCursor.valid($pos)) return $pos
      let pos = $pos.pos, next = null
      // Scan up from this position
      for (let d = $pos.depth;; d--) {
        let parent = $pos.node(d)
        if (dir > 0 ? $pos.indexAfter(d) < parent.childCount : $pos.index(d) > 0) {
          next = parent.child(dir > 0 ? $pos.indexAfter(d) : $pos.index(d) - 1)
          break
        } else if (d == 0) {
          return null
        }
        pos += dir
        let $cur = $pos.doc.resolve(pos)
        if (GapCursor.valid($cur)) return $cur
      }

      // And then down into the next node
      for (;;) {
        let inside: Node | null = dir > 0 ? next.firstChild : next.lastChild
        if (!inside) {
          if (next.isAtom && !next.isText && !NodeSelection.isSelectable(next)) {
            $pos = $pos.doc.resolve(pos + next.nodeSize * dir)
            mustMove = false
            continue search
          }
          break
        }
        next = inside
        pos += dir
        let $cur = $pos.doc.resolve(pos)
        if (GapCursor.valid($cur)) return $cur
      }

      return null
    }
  }
}

GapCursor.prototype.visible = false

;(GapCursor as any).findFrom = GapCursor.findGapCursorFrom

Selection.jsonID("gapcursor", GapCursor)

class GapBookmark {
  constructor(readonly pos: number) {}

  map(mapping: Mappable) {
    return new GapBookmark(mapping.map(this.pos))
  }
  resolve(doc: Node) {
    let $pos = doc.resolve(this.pos)
    return GapCursor.valid($pos) ? new GapCursor($pos) : Selection.near($pos)
  }
}

function closedBefore($pos: ResolvedPos) {
  for (let d = $pos.depth; d >= 0; d--) {
    let index = $pos.index(d), parent = $pos.node(d)
    // At the start of this parent, look at next one
    if (index == 0) {
      if (parent.type.spec.isolating) return true
      continue
    }
    // See if the node before (or its first ancestor) is closed
    for (let before = parent.child(index - 1);; before = before.lastChild!) {
      if ((before.childCount == 0 && !before.inlineContent) || before.isAtom || before.type.spec.isolating) return true
      if (before.inlineContent) return false
    }
  }
  // Hit start of document
  return true
}

function closedAfter($pos: ResolvedPos) {
  for (let d = $pos.depth; d >= 0; d--) {
    let index = $pos.indexAfter(d), parent = $pos.node(d)
    if (index == parent.childCount) {
      if (parent.type.spec.isolating) return true
      continue
    }
    for (let after = parent.child(index);; after = after.firstChild!) {
      if ((after.childCount == 0 && !after.inlineContent) || after.isAtom || after.type.spec.isolating) return true
      if (after.inlineContent) return false
    }
  }
  return true
}
