import { Fragment, Node, Slice } from '@atlaskit/editor-prosemirror/model';
import { NodeSelection, Selection, TextSelection } from '@atlaskit/editor-prosemirror/state';
import { ReplaceAroundStep, ReplaceStep } from '@atlaskit/editor-prosemirror/transform';
import { canInsert, findParentNodeOfType, hasParentNodeOfType, isNodeSelection, safeInsert as pmSafeInsert } from '@atlaskit/editor-prosemirror/utils';
import { GapCursorSelection, Side } from '../selection';
import { isEmptyParagraph, isListItemNode } from '../utils';
export let LookDirection = /*#__PURE__*/function (LookDirection) {
  LookDirection["Before"] = "before";
  LookDirection["After"] = "after";
  return LookDirection;
}({});
export const normaliseNestedLayout = ({
  selection,
  doc
}, node) => {
  if (selection.$from.depth > 1) {
    if (node.attrs.layout && node.attrs.layout !== 'default') {
      return node.type.createChecked({
        ...node.attrs,
        layout: 'default'
      }, node.content, node.marks);
    }

    // If its a breakout layout, we can remove the mark
    // Since default isn't a valid breakout mode.
    const breakoutMark = doc.type.schema.marks.breakout;
    if (breakoutMark && breakoutMark.isInSet(node.marks)) {
      const newMarks = breakoutMark.removeFromSet(node.marks);
      return node.type.createChecked(node.attrs, node.content, newMarks);
    }
  }
  return node;
};
const isLastChild = ($pos, doc) => doc.resolve($pos.after()).node().lastChild === $pos.node();
const isFirstChild = ($pos, doc) => doc.resolve($pos.before()).node().firstChild === $pos.node();
const nodeIsInsideAList = tr => {
  const {
    nodes
  } = tr.doc.type.schema;
  return hasParentNodeOfType([nodes.orderedList, nodes.bulletList])(tr.selection);
};
const selectionIsInsideAPanel = tr => {
  const {
    nodes
  } = tr.doc.type.schema;
  return hasParentNodeOfType(nodes.panel)(tr.selection);
};
const selectionIsInNestedList = tr => {
  const {
    nodes
  } = tr.doc.type.schema;
  const parentListNode = findParentNodeOfType([nodes.orderedList, nodes.bulletList])(tr.selection);
  if (!parentListNode) {
    return false;
  }
  return isListItemNode(tr.doc.resolve(parentListNode.pos).parent);
};
const insertBeforeOrAfter = (tr, lookDirection, $parentPos, $proposedPosition, content) => {
  /**
   * This block caters for the first item in a parent with the cursor being at the very start
   * or the last item with the cursor being at the very end
   *
   * e.g.
   * ul
   *  li {<>}Scenario one
   *  li
   *  li Scenario two{<>}
   */

  if (isFirstChild($proposedPosition, tr.doc) && lookDirection === LookDirection.Before || isLastChild($proposedPosition, tr.doc) && lookDirection === LookDirection.After) {
    return tr.insert($parentPos[lookDirection](), content);
  }
  return tr.insert($proposedPosition[lookDirection](), content);
};

// FIXME: A more sustainable and configurable way to choose when to split
const shouldSplit = (nodeType, schemaNodes) => {
  return [schemaNodes.bulletList, schemaNodes.orderedList, schemaNodes.panel].includes(nodeType);
};
export const safeInsert = (content, position) => tr => {
  var _tr$selection$$from$n;
  const {
    nodes
  } = tr.doc.type.schema;
  const whitelist = [nodes.rule, nodes.mediaSingle];
  if (content instanceof Fragment || !whitelist.includes(content.type)) {
    return null;
  }

  // Check for selection
  if (!tr.selection.empty || isNodeSelection(tr.selection)) {
    // NOT IMPLEMENTED
    return null;
  }
  const {
    $from
  } = tr.selection;
  const $insertPos = position ? tr.doc.resolve(position) : isNodeSelection(tr.selection) ? tr.doc.resolve($from.pos + 1) : $from;
  let lookDirection;
  const insertPosEnd = $insertPos.end();
  const insertPosStart = $insertPos.start();

  // When parent node is an empty paragraph,
  // check the empty paragraph is the first or last node of its parent.
  if (isEmptyParagraph($insertPos.parent)) {
    if (isLastChild($insertPos, tr.doc)) {
      lookDirection = LookDirection.After;
    } else if (isFirstChild($insertPos, tr.doc)) {
      lookDirection = LookDirection.Before;
    }
  } else {
    if ($insertPos.pos === insertPosEnd) {
      lookDirection = LookDirection.After;
    } else if ($insertPos.pos === insertPosStart) {
      lookDirection = LookDirection.Before;
    }
  }
  const grandParentNodeType = (_tr$selection$$from$n = tr.selection.$from.node(-1)) === null || _tr$selection$$from$n === void 0 ? void 0 : _tr$selection$$from$n.type;
  const parentNodeType = tr.selection.$from.parent.type;

  // if there is no direction, and cannot split for this particular node
  const noDirectionAndShouldNotSplit = !lookDirection && !shouldSplitSelectedNodeOnNodeInsertion({
    parentNodeType,
    grandParentNodeType,
    content
  });
  const ruleNodeInANestedListNode = content.type === nodes.rule && selectionIsInNestedList(tr);
  const nonRuleNodeInListNode = !(content.type === nodes.rule) && nodeIsInsideAList(tr);
  if (ruleNodeInANestedListNode || noDirectionAndShouldNotSplit && nonRuleNodeInListNode || noDirectionAndShouldNotSplit && !nodeIsInsideAList(tr)) {
    // node to be inserted is an invalid child of selection so insert below selected node
    return pmSafeInsert(content, tr.selection.from)(tr);
  }

  // if node is a rule and that is a flat list splitting and not at the end of a list
  const {
    from,
    to
  } = tr.selection;
  const ruleTypeInAList = content.type === nodes.rule && nodeIsInsideAList(tr);
  if (ruleTypeInAList && !($insertPos.pos === insertPosEnd)) {
    return tr.replaceRange(from, to, new Slice(Fragment.from(nodes.rule.createChecked()), 0, 0));
  }
  if (!lookDirection) {
    // fallback to consumer for now
    return null;
  }

  // Replace empty paragraph
  if (isEmptyParagraph($insertPos.parent) && canInsert(tr.doc.resolve($insertPos[lookDirection]()), content)) {
    return finaliseInsert(tr.replaceWith($insertPos.before(), $insertPos.after(), content), -1);
  }
  let $proposedPosition = $insertPos;
  while ($proposedPosition.depth > 0) {
    const $parentPos = tr.doc.resolve($proposedPosition[lookDirection]());
    const parentNode = $parentPos.node();

    // Insert at position (before or after target pos)
    if (canInsert($proposedPosition, content)) {
      return finaliseInsert(tr.insert($proposedPosition.pos, content), content.nodeSize);
    }

    // If we can't insert, and we think we should split, we fallback to consumer for now
    if (shouldSplit(parentNode.type, tr.doc.type.schema.nodes)) {
      const nextTr = finaliseInsert(insertBeforeOrAfter(tr, lookDirection, $parentPos, $proposedPosition, content), content.nodeSize);

      // Move selection to the closest text node, otherwise it defaults to the whatever the lookDirection is set to above
      if ([nodes.orderedList, nodes.bulletList].includes(parentNode.type) && nextTr) {
        return nextTr.setSelection(TextSelection.between(nextTr.selection.$from, nextTr.selection.$from));
      } else {
        return nextTr;
      }
    }

    // Can not insert into current parent, step up one parent
    $proposedPosition = $parentPos;
  }
  return finaliseInsert(tr.insert($proposedPosition.pos, content), content.nodeSize);
};
const finaliseInsert = (tr, nodeLength) => {
  const lastStep = tr.steps[tr.steps.length - 1];
  if (!(lastStep instanceof ReplaceStep || lastStep instanceof ReplaceAroundStep)) {
    return null;
  }

  // Place gap cursor after the newly inserted node
  const gapCursorPos = lastStep.to + lastStep.slice.openStart + nodeLength;
  return tr.setSelection(new GapCursorSelection(tr.doc.resolve(gapCursorPos), Side.RIGHT)).scrollIntoView();
};

/**
 * Method extracted from typeahead plugin to be shared with the element browser on handling element insertion.
 */
export const insertSelectedItem = (maybeNode, opts = {}) => (state, tr, start) => {
  if (!maybeNode) {
    return tr;
  }
  const isInputFragment = maybeNode instanceof Fragment;
  let node;
  try {
    node = maybeNode instanceof Node || isInputFragment ? maybeNode : typeof maybeNode === 'string' ? state.schema.text(maybeNode) : Node.fromJSON(state.schema, maybeNode);
  } catch (e) {
    // eslint-disable-next-line no-console
    console.error(e);
    return tr;
  }
  if (node instanceof Node && node.isText) {
    tr = tr.replaceWith(start, start, node);

    /**
     *
     * Replacing a type ahead query mark with a block node.
     *
     */
  } else if (node instanceof Node && node.isBlock) {
    /**
     *
     * Rule has unique insertion behaviour
     * so using this safeInsert function in order to handle specific cases in flat list vs nested list
     * instead of a generic pmSafeInsert (i.e appending at the end)
     *
     */

    const selectionInsideAPanel = selectionIsInsideAPanel(tr);
    if (node.type.name === 'rule' && !selectionInsideAPanel &&
    // ED-17438 If the selection is not an empty paragraph we want to use pmSafeInsert
    // This fixes a bug where if a rule was inserted using safeInsert and the selection
    // was an empty paragraph it would not be inserted
    !isEmptyParagraph(tr.selection.$from.parent)) {
      var _safeInsert;
      tr = (_safeInsert = safeInsert(node, tr.selection.from)(tr)) !== null && _safeInsert !== void 0 ? _safeInsert : tr;
    } else {
      tr = pmSafeInsert(normaliseNestedLayout(state, node), undefined, true)(tr);
    }

    /**
     *
     * Replacing a type ahead query mark with an inline node.
     *
     */
  } else if (node instanceof Node && node.isInline || isInputFragment) {
    const fragment = isInputFragment ? node : Fragment.fromArray([node, state.schema.text(' ')]);
    tr = tr.replaceWith(start, start, fragment);
    if (opts.selectInlineNode) {
      // Select inserted node
      tr = tr.setSelection(NodeSelection.create(tr.doc, start));
    } else {
      // Placing cursor after node + space.
      tr = tr.setSelection(Selection.near(tr.doc.resolve(start + fragment.size)));
    }
  }
  return tr;
};

/**
 * ED-14584: Util to check if the destination node is a paragraph & the
 * content being inserted is a valid child of the grandparent node.
 * In this case, the destination node should split
 */
export const shouldSplitSelectedNodeOnNodeInsertion = ({
  parentNodeType,
  grandParentNodeType,
  content
}) => {
  if (parentNodeType.name === 'doc' || parentNodeType.name === 'paragraph' && grandParentNodeType.validContent(Fragment.from(content))) {
    return true;
  }
  return false;
};