/*
This file is part of the Notesnook project (https://notesnook.com/)

Copyright (C) 2023 Streetwriters (Private) Limited

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.
*/
import { findParentNode, getChangedRanges } from "@tiptap/core";
import { Slice } from "prosemirror-model";
import { BulletList } from "../extensions/bullet-list";
import { ListItem } from "../extensions/list-item";
import { OrderedList } from "../extensions/ordered-list";
import { OutlineList } from "../extensions/outline-list";
import { OutlineListItem } from "../extensions/outline-list-item";
import { TaskItemNode } from "../extensions/task-item";
import { TaskListNode } from "../extensions/task-list";
import { LIST_NODE_TYPES } from "./node-types";
import TextStyle from "@tiptap/extension-text-style";
import CheckList from "../extensions/check-list";
import CheckListItem from "../extensions/check-list-item";
export function hasSameAttributes(prev, next) {
    for (const key in prev) {
        const prevValue = prev[key];
        const nextValue = next[key];
        if (prevValue !== nextValue)
            return false;
    }
    return true;
}
export function findListItemType(editor) {
    const isTaskList = editor.isActive(TaskListNode.name);
    const isCheckList = editor.isActive(CheckList.name);
    const isOutlineList = editor.isActive(OutlineList.name);
    const isList = editor.isActive(BulletList.name) || editor.isActive(OrderedList.name);
    return isList
        ? ListItem.name
        : isOutlineList
            ? OutlineListItem.name
            : isTaskList
                ? TaskItemNode.name
                : isCheckList
                    ? CheckListItem.name
                    : null;
}
export function isListActive(editor) {
    return LIST_NODE_TYPES.some((name) => editor.isActive(name));
}
export function findSelectedDOMNode(editor, types) {
    var _a;
    const { $anchor } = editor.state.selection;
    const selectedNode = editor.state.doc.nodeAt($anchor.pos);
    const pos = types.includes((selectedNode === null || selectedNode === void 0 ? void 0 : selectedNode.type.name) || "")
        ? $anchor.pos
        : (_a = findParentNode((node) => types.includes(node.type.name))(editor.state.selection)) === null || _a === void 0 ? void 0 : _a.pos;
    if (!pos)
        return null;
    return editor.view.nodeDOM(pos) || null;
}
export function findSelectedNode(editor, type) {
    var _a;
    const { $anchor } = editor.state.selection;
    const selectedNode = editor.state.doc.nodeAt($anchor.pos);
    const pos = (selectedNode === null || selectedNode === void 0 ? void 0 : selectedNode.type.name) === type
        ? $anchor.pos
        : (_a = findParentNode((node) => node.type.name === type)(editor.state.selection)) === null || _a === void 0 ? void 0 : _a.pos;
    if (pos === undefined)
        return null;
    return editor.state.doc.nodeAt(pos);
}
export function findMark(node, type) {
    const mark = node.marks.find((m) => m.type.name === type);
    return mark;
}
export function selectionToOffset(state) {
    const { from, $from, to, $to } = state.selection;
    const node = state.doc.nodeAt(from);
    return {
        node: node || undefined,
        from: from - $from.textOffset,
        to: node ? from - $from.textOffset + node.nodeSize : to - $to.textOffset
    };
}
export const findChildren = (node, predicate, descend) => {
    if (!node) {
        throw new Error('Invalid "node" parameter');
    }
    else if (!predicate) {
        throw new Error('Invalid "predicate" parameter');
    }
    return walkNode(node, descend).filter((child) => predicate(child.node));
};
export function findChildrenByType(node, nodeType, descend = true) {
    return findChildren(node, (child) => child.type === nodeType, descend);
}
export const findParentNodeOfTypeClosestToPos = ($pos, nodeType) => {
    return findParentNodeClosestToPos($pos, (node) => equalNodeType(nodeType, node));
};
export function findParentNodeClosestToPos($pos, predicate) {
    for (let i = $pos.depth; i > 0; i -= 1) {
        const node = $pos.node(i);
        if (predicate(node, i === 1 ? undefined : $pos.node(i - 1))) {
            return {
                pos: i > 0 ? $pos.before(i) : 0,
                start: $pos.start(i),
                depth: i,
                node
            };
        }
    }
}
export function hasParentNode(predicate) {
    return function (selection) {
        return !!findParentNode(predicate)(selection);
    };
}
export function hasParentNodeOfType(nodeType) {
    return hasParentNode((node) => equalNodeType(nodeType, node));
}
export function findParentNodeOfType(nodeType) {
    return findParentNode((node) => equalNodeType(nodeType, node));
}
export function getParentAttributes(editor, keepMarks = false, keepAttributes = false) {
    var _a;
    const { textAlign, textDirection } = ((_a = editor.state.selection.$from.parent) === null || _a === void 0 ? void 0 : _a.attrs) || {};
    return Object.assign(Object.assign({}, (keepMarks || keepAttributes
        ? editor.getAttributes(TextStyle.name)
        : {})), { textAlign,
        textDirection });
}
const walkNode = (node, descend = true) => {
    if (!node) {
        throw new Error('Invalid "node" parameter');
    }
    const result = [];
    node.descendants((child, pos) => {
        result.push({ node: child, pos });
        if (!descend) {
            return false;
        }
    });
    return result;
};
const equalNodeType = (nodeType, node) => {
    return ((Array.isArray(nodeType) && nodeType.indexOf(node.type) > -1) ||
        node.type === nodeType);
};
export function getChangedNodeRanges(tr) {
    // The container of the ranges to be returned from this function.
    const nodeRanges = [];
    const ranges = getChangedRanges(tr);
    for (const range of ranges) {
        try {
            const $from = tr.doc.resolve(range.newRange.from);
            const $to = tr.doc.resolve(range.newRange.to);
            // Find the node range for this provided range.
            const nodeRange = $from.blockRange($to);
            // Make sure a valid node is available.
            if (nodeRange) {
                nodeRanges.push(nodeRange);
            }
        }
        catch (_a) {
            // Changed ranged outside the document
        }
    }
    return nodeRanges;
}
/**
 * Get all the changed nodes from the provided transaction.
 *
 * The following example will give us all the text nodes in the provided
 * transaction.
 *
 * ```ts
 * import { getChangedNodes } from 'remirror/core';
 *
 * const changedTextNodes = getChangeNodes(tr, { descend: true, predicate: (node) => node.isText });
 * ```
 */
export function getChangedNodes(tr, options = {}) {
    const { descend = false, predicate } = options;
    const nodeRange = getChangedNodeRanges(tr);
    // The container for the nodes which have been added..
    const nodes = [];
    for (const range of nodeRange) {
        const { start, end } = range;
        // Find all the nodes between the provided node range.
        tr.doc.nodesBetween(start, end, (node, pos) => {
            // Check wether this is a node that should be added.
            const shouldAdd = !predicate || predicate(node, start, range);
            if (shouldAdd && nodes.every((n) => n.pos !== pos)) {
                nodes.push({ node, pos });
            }
            return descend;
        });
    }
    return nodes;
}
export function getExactChangedNodes(tr, predicate) {
    const nodeRange = getChangedNodeRanges(tr);
    // The container for the nodes which have been added..
    const nodes = [];
    for (const range of nodeRange) {
        const { start } = range;
        if (nodeRange && nodes.every((n) => n.pos !== start)) {
            const node = tr.doc.nodeAt(start);
            if (node && (!predicate || predicate(node, start, range)))
                nodes.push({ node, pos: start });
        }
    }
    return nodes;
}
export function getDeletedNodes(tr, predicate) {
    const nodes = [];
    for (const step of tr.steps) {
        if ("slice" in step &&
            step.slice instanceof Slice &&
            "to" in step &&
            typeof step.to === "number" &&
            "from" in step &&
            typeof step.from === "number" &&
            step.from < tr.doc.nodeSize - 1 &&
            step.slice === Slice.empty) {
            const $from = tr.doc.resolve(step.from);
            const node = findParentNodeClosestToPos($from, predicate);
            if (node)
                nodes.push(node);
        }
    }
    return nodes;
}
