import { deleteNearSelection } from "./deleteNearSelection"
import { runInOp } from "../display/operations"
import { ensureCursorVisible } from "../display/scrolling"
import { clipPos, Pos } from "../line/pos"
import { collapsedSpanAtEnd, visualLine } from "../line/spans"
import { getLine, lineNo } from "../line/utils_line"
import { Range } from "../model/selection"
import { selectAll } from "../model/selection_updates"
import { countColumn, sel_dontScroll, sel_move, spaceStr } from "../util/misc"
import { getOrder, lineLeft, lineRight } from "../util/bidi"

// Commands are parameter-less actions that can be performed on an
// editor, mostly used for keybindings.
export let commands = {
  selectAll: selectAll,
  singleSelection: cm => cm.setSelection(cm.getCursor("anchor"), cm.getCursor("head"), sel_dontScroll),
  killLine: cm => deleteNearSelection(cm, range => {
    if (range.empty()) {
      let len = getLine(cm.doc, range.head.line).text.length
      if (range.head.ch == len && range.head.line < cm.lastLine())
        return {from: range.head, to: Pos(range.head.line + 1, 0)}
      else
        return {from: range.head, to: Pos(range.head.line, len)}
    } else {
      return {from: range.from(), to: range.to()}
    }
  }),
  deleteLine: cm => deleteNearSelection(cm, range => ({
    from: Pos(range.from().line, 0),
    to: clipPos(cm.doc, Pos(range.to().line + 1, 0))
  })),
  delLineLeft: cm => deleteNearSelection(cm, range => ({
    from: Pos(range.from().line, 0), to: range.from()
  })),
  delWrappedLineLeft: cm => deleteNearSelection(cm, range => {
    let top = cm.charCoords(range.head, "div").top + 5
    let leftPos = cm.coordsChar({left: 0, top: top}, "div")
    return {from: leftPos, to: range.from()}
  }),
  delWrappedLineRight: cm => deleteNearSelection(cm, range => {
    let top = cm.charCoords(range.head, "div").top + 5
    let rightPos = cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div")
    return {from: range.from(), to: rightPos }
  }),
  undo: cm => cm.undo(),
  redo: cm => cm.redo(),
  undoSelection: cm => cm.undoSelection(),
  redoSelection: cm => cm.redoSelection(),
  goDocStart: cm => cm.extendSelection(Pos(cm.firstLine(), 0)),
  goDocEnd: cm => cm.extendSelection(Pos(cm.lastLine())),
  goLineStart: cm => cm.extendSelectionsBy(range => lineStart(cm, range.head.line),
    {origin: "+move", bias: 1}
  ),
  goLineStartSmart: cm => cm.extendSelectionsBy(range => lineStartSmart(cm, range.head),
    {origin: "+move", bias: 1}
  ),
  goLineEnd: cm => cm.extendSelectionsBy(range => lineEnd(cm, range.head.line),
    {origin: "+move", bias: -1}
  ),
  goLineRight: cm => cm.extendSelectionsBy(range => {
    let top = cm.charCoords(range.head, "div").top + 5
    return cm.coordsChar({left: cm.display.lineDiv.offsetWidth + 100, top: top}, "div")
  }, sel_move),
  goLineLeft: cm => cm.extendSelectionsBy(range => {
    let top = cm.charCoords(range.head, "div").top + 5
    return cm.coordsChar({left: 0, top: top}, "div")
  }, sel_move),
  goLineLeftSmart: cm => cm.extendSelectionsBy(range => {
    let top = cm.charCoords(range.head, "div").top + 5
    let pos = cm.coordsChar({left: 0, top: top}, "div")
    if (pos.ch < cm.getLine(pos.line).search(/\S/)) return lineStartSmart(cm, range.head)
    return pos
  }, sel_move),
  goLineUp: cm => cm.moveV(-1, "line"),
  goLineDown: cm => cm.moveV(1, "line"),
  goPageUp: cm => cm.moveV(-1, "page"),
  goPageDown: cm => cm.moveV(1, "page"),
  goCharLeft: cm => cm.moveH(-1, "char"),
  goCharRight: cm => cm.moveH(1, "char"),
  goColumnLeft: cm => cm.moveH(-1, "column"),
  goColumnRight: cm => cm.moveH(1, "column"),
  goWordLeft: cm => cm.moveH(-1, "word"),
  goGroupRight: cm => cm.moveH(1, "group"),
  goGroupLeft: cm => cm.moveH(-1, "group"),
  goWordRight: cm => cm.moveH(1, "word"),
  delCharBefore: cm => cm.deleteH(-1, "char"),
  delCharAfter: cm => cm.deleteH(1, "char"),
  delWordBefore: cm => cm.deleteH(-1, "word"),
  delWordAfter: cm => cm.deleteH(1, "word"),
  delGroupBefore: cm => cm.deleteH(-1, "group"),
  delGroupAfter: cm => cm.deleteH(1, "group"),
  indentAuto: cm => cm.indentSelection("smart"),
  indentMore: cm => cm.indentSelection("add"),
  indentLess: cm => cm.indentSelection("subtract"),
  insertTab: cm => cm.replaceSelection("\t"),
  insertSoftTab: cm => {
    let spaces = [], ranges = cm.listSelections(), tabSize = cm.options.tabSize
    for (let i = 0; i < ranges.length; i++) {
      let pos = ranges[i].from()
      let col = countColumn(cm.getLine(pos.line), pos.ch, tabSize)
      spaces.push(spaceStr(tabSize - col % tabSize))
    }
    cm.replaceSelections(spaces)
  },
  defaultTab: cm => {
    if (cm.somethingSelected()) cm.indentSelection("add")
    else cm.execCommand("insertTab")
  },
  // Swap the two chars left and right of each selection's head.
  // Move cursor behind the two swapped characters afterwards.
  //
  // Doesn't consider line feeds a character.
  // Doesn't scan more than one line above to find a character.
  // Doesn't do anything on an empty line.
  // Doesn't do anything with non-empty selections.
  transposeChars: cm => runInOp(cm, () => {
    let ranges = cm.listSelections(), newSel = []
    for (let i = 0; i < ranges.length; i++) {
      if (!ranges[i].empty()) continue
      let cur = ranges[i].head, line = getLine(cm.doc, cur.line).text
      if (line) {
        if (cur.ch == line.length) cur = new Pos(cur.line, cur.ch - 1)
        if (cur.ch > 0) {
          cur = new Pos(cur.line, cur.ch + 1)
          cm.replaceRange(line.charAt(cur.ch - 1) + line.charAt(cur.ch - 2),
                          Pos(cur.line, cur.ch - 2), cur, "+transpose")
        } else if (cur.line > cm.doc.first) {
          let prev = getLine(cm.doc, cur.line - 1).text
          if (prev) {
            cur = new Pos(cur.line, 1)
            cm.replaceRange(line.charAt(0) + cm.doc.lineSeparator() +
                            prev.charAt(prev.length - 1),
                            Pos(cur.line - 1, prev.length - 1), cur, "+transpose")
          }
        }
      }
      newSel.push(new Range(cur, cur))
    }
    cm.setSelections(newSel)
  }),
  newlineAndIndent: cm => runInOp(cm, () => {
    let sels = cm.listSelections()
    for (let i = sels.length - 1; i >= 0; i--)
      cm.replaceRange(cm.doc.lineSeparator(), sels[i].anchor, sels[i].head, "+input")
    sels = cm.listSelections()
    for (let i = 0; i < sels.length; i++)
      cm.indentLine(sels[i].from().line, null, true)
    ensureCursorVisible(cm)
  }),
  openLine: cm => cm.replaceSelection("\n", "start"),
  toggleOverwrite: cm => cm.toggleOverwrite()
}


function lineStart(cm, lineN) {
  let line = getLine(cm.doc, lineN)
  let visual = visualLine(line)
  if (visual != line) lineN = lineNo(visual)
  let order = getOrder(visual)
  let ch = !order ? 0 : order[0].level % 2 ? lineRight(visual) : lineLeft(visual)
  return Pos(lineN, ch)
}
function lineEnd(cm, lineN) {
  let merged, line = getLine(cm.doc, lineN)
  while (merged = collapsedSpanAtEnd(line)) {
    line = merged.find(1, true).line
    lineN = null
  }
  let order = getOrder(line)
  let ch = !order ? line.text.length : order[0].level % 2 ? lineLeft(line) : lineRight(line)
  return Pos(lineN == null ? lineNo(line) : lineN, ch)
}
function lineStartSmart(cm, pos) {
  let start = lineStart(cm, pos.line)
  let line = getLine(cm.doc, start.line)
  let order = getOrder(line)
  if (!order || order[0].level == 0) {
    let firstNonWS = Math.max(0, line.text.search(/\S/))
    let inWS = pos.line == start.line && pos.ch <= firstNonWS && pos.ch
    return Pos(start.line, inWS ? 0 : firstNonWS)
  }
  return start
}
