import { Editor, Operation, Node, Path, Text, Descendant, NodeEntry, Transforms as SlateTransforms, Location } from 'slate';
import { NODE_TO_KEY } from 'slate-vue-shared';
import Vue from 'vue'

export const getChildren = (node: Node): any => {
  return Editor.isEditor(node) ? (node as any)._state: (node as any).children
}

export const clone = (node: any): any => {
  return JSON.parse(JSON.stringify(node))
}

// a minimum version of Editor.transform for runtime
export const transform = function(editor: Editor, op: Operation) {
  switch (op.type) {
    case 'insert_node': {
      const { path, node } = op
      const parent = Node.parent(editor, path)
      const index = path[path.length - 1]
      getChildren(parent).splice(index, 0, clone(node))

      break
    }

    case 'insert_text': {
      const { path, offset, text } = op
      const node = Node.leaf(editor, path)
      const before = node.text.slice(0, offset)
      const after = node.text.slice(offset)
      node.text = before + text + after

      break
    }

    case 'merge_node': {
      const { path } = op
      const node = Node.get(editor, path)
      const prevPath = Path.previous(path)
      const prev = Node.get(editor, prevPath)
      const parent = Node.parent(editor, path)
      const index = path[path.length - 1]

      if (Text.isText(node) && Text.isText(prev)) {
        prev.text += node.text
      } else if (!Text.isText(node) && !Text.isText(prev)) {
        getChildren(prev).push(...getChildren(node))
      } else {
        throw new Error(
          `Cannot apply a "merge_node" operation at path [${path}] to nodes of different interaces: ${node} ${prev}`
        )
      }

      getChildren(parent).splice(index, 1)

      break
    }

    case 'move_node': {
      const { path, newPath } = op

      if (Path.isAncestor(path, newPath)) {
        throw new Error(
          `Cannot move a path [${path}] to new path [${newPath}] because the destination is inside itself.`
        )
      }

      const node = Node.get(editor, path)
      const parent = Node.parent(editor, path)
      const index = path[path.length - 1]

      // This is tricky, but since the `path` and `newPath` both refer to
      // the same snapshot in time, there's a mismatch. After either
      // removing the original position, the second step's path can be out
      // of date. So instead of using the `op.newPath` directly, we
      // transform `op.path` to ascertain what the `newPath` would be after
      // the operation was applied.
      getChildren(parent).splice(index, 1)
      const truePath = Path.transform(path, op)!
      const newParent = Node.get(editor, Path.parent(truePath))
      const newIndex = truePath[truePath.length - 1]

      getChildren(newParent).splice(newIndex, 0, node)

      break
    }

    case 'remove_node': {
      const { path } = op
      NODE_TO_KEY.delete(Node.get(editor, path))
      const index = path[path.length - 1]
      const parent = Node.parent(editor, path)
      getChildren(parent).splice(index, 1)

      break
    }

    case 'remove_text': {
      const { path, offset, text } = op
      const node = Node.leaf(editor, path)
      const before = node.text.slice(0, offset)
      const after = node.text.slice(offset + text.length)
      node.text = before + after

      break
    }

    case 'set_node': {
      const { path, newProperties } = op

      if (path.length === 0) {
        throw new Error(`Cannot set properties on the root node!`)
      }

      const node = Node.get(editor, path)

      for (const key in newProperties) {
        if (key === 'children' || key === 'text') {
          throw new Error(`Cannot set the "${key}" property of nodes!`)
        }

        const value = (newProperties as any)[key]

        if (value == null) {
          Vue.delete(node, key)
        } else {
          Vue.set(node, key, value)
        }
      }

      break
    }

    case 'set_selection': {
      break
    }

    case 'split_node': {
      const { path, position, properties } = op

      if (path.length === 0) {
        throw new Error(
          `Cannot apply a "split_node" operation at path [${path}] because the root node cannot be split.`
        )
      }

      const node = Node.get(editor, path)
      const parent = Node.parent(editor, path)
      const index = path[path.length - 1]
      let newNode: Descendant

      if (Text.isText(node)) {
        const before = node.text.slice(0, position)
        const after = node.text.slice(position)
        node.text = before
        newNode = {
          ...node,
          ...(properties as Partial<Text>),
          text: after,
        }
      } else {
        const before = node.children.slice(0, position)
        const after = node.children.slice(position)
        node.children = before

        newNode = {
          ...node,
          ...(properties as Partial<Element>),
          children: after,
        }
      }

      getChildren(parent).splice(index + 1, 0, newNode)

      break
    }
  }
}

// a minimum version of Node for runtime
export const runtimeNode = {
  child(root: Node, index: number): Descendant  {
    if (Text.isText(root)) {
      throw new Error(
        `Cannot get the child of a text node: ${JSON.stringify(root)}`
      )
    }

    const c = getChildren(root)[index] as Descendant

    if (c == null) {
      throw new Error(
        `Cannot get child at index \`${index}\` in node: ${JSON.stringify(
          root
        )}`
      )
    }

    return c
  },
  has(root: Node, path: Path): boolean {
    let node = root

    for (let i = 0; i < path.length; i++) {
      const p = path[i]
      const children = getChildren(node)
      if (Text.isText(node) || !children[p]) {
        return false
      }

      node = children[p]
    }

    return true
},
  get(root: Node, path: Path): Node {
    let node = root

    for (let i = 0; i < path.length; i++) {
      const p = path[i]
      const children = getChildren(node)

      if (Text.isText(node) || !children[p]) {
        throw new Error(
          `Cannot find a descendant at path [${path}] in node: ${JSON.stringify(
            root
          )}`
        )
      }

      node = children[p]
    }

    return node
  },
  first(root: Node, path: Path): NodeEntry {
    const p = path.slice()
    let n = Node.get(root, p)
    const children = getChildren(n)

    while (n) {
      if (Text.isText(n) || children.length === 0) {
        break
      } else {
        n = children[0]
        p.push(0)
      }
    }

    return [n, p]
  },
  last(root: Node, path: Path): NodeEntry {
    const p = path.slice()
    let n = Node.get(root, p)
    const children = getChildren(n)

    while (n) {
      if (Text.isText(n) || children.length === 0) {
        break
      } else {
        const i = children.length - 1
        n = children[i]
        p.push(i)
      }
    }

    return [n, p]
  },
  *nodes(
    root: Node,
    options: {
      from?: Path
      to?: Path
      reverse?: boolean
      pass?: (entry: NodeEntry) => boolean
    } = {}
  ): Generator<NodeEntry> {
    const { pass, reverse = false } = options
    const { from = [], to } = options
    const visited = new Set()
    let p: Path = []
    let n = root

    while (true) {
      if (to && (reverse ? Path.isBefore(p, to) : Path.isAfter(p, to))) {
        break
      }

      if (!visited.has(n)) {
        yield [n, p]
      }

      // If we're allowed to go downward and we haven't decsended yet, do.
      if (
        !visited.has(n) &&
        !Text.isText(n) &&
        getChildren(n).length !== 0 &&
        (pass == null || pass([n, p]) === false)
      ) {
        visited.add(n)
        let nextIndex = reverse ? getChildren(n).length - 1 : 0

        if (Path.isAncestor(p, from)) {
          nextIndex = from[p.length]
        }

        p = p.concat(nextIndex)
        n = Node.get(root, p)
        continue
      }

      // If we're at the root and we can't go down, we're done.
      if (p.length === 0) {
        break
      }

      // If we're going forward...
      if (!reverse) {
        const newPath = Path.next(p)

        if (Node.has(root, newPath)) {
          p = newPath
          n = Node.get(root, p)
          continue
        }
      }

      // If we're going backward...
      if (reverse && p[p.length - 1] !== 0) {
        const newPath = Path.previous(p)
        p = newPath
        n = Node.get(root, p)
        continue
      }

      // Otherwise we're going upward...
      p = Path.parent(p)
      n = Node.get(root, p)
      visited.add(n)
    }
  }
}

export const isVueObject = (obj: any) => {
  return obj.__ob__
}

// a Transform version for runtime
export const Transforms = (() => {
  const {select} = SlateTransforms
  SlateTransforms.select = (editor: Editor, target: Location) => {
    if(isVueObject(target)) {
      target = clone(target)
    }
    return select(editor, target)
  }
  return SlateTransforms
})()