import * as A from "fp-ts-esm/Array";
import { flow, identity, pipe } from "fp-ts-esm/function";
import { not } from "fp-ts-esm/Predicate";
import * as T from "fp-ts-esm/Tuple";
import { generateId } from "./../converters/generate-random-id.js";
import { Node_Type } from "./../../ricos-schema/legacy/wix/rich_content/v1/index.js";
import { and } from "../fp-utils.js";
import { extract } from "../RicosContentAPI/extract.js";
import { modify } from "../RicosContentAPI/modify.js";
import { fromEntries } from "../utils.js";
import { DEFAULT_COUNTABLE_TYPES, isNonTextual, isTextContainer as isTextual } from "./types.js";
const visitNodes = (visitorFn) => ({ nodes }) => {
  const nodesToTasks = (nodes2, parents) => nodes2.map((node, i, nodes3) => [node, { parents, previousSibling: nodes3[i - 1] }]);
  let tasks = nodesToTasks(nodes ?? [], []);
  while (tasks.length) {
    const [node, context] = tasks.shift();
    visitorFn(node, context);
    const parents = [node, ...context.parents];
    tasks = [...nodesToTasks(node.nodes || [], parents), ...tasks];
  }
};
const getFilteredNodes = (filterFn) => (container) => {
  const result = [];
  visitNodes((node, context) => {
    if (filterFn(node, context)) {
      result.push(node);
    }
  })(container);
  return result;
};
const isNonTextualNode = (node) => isNonTextual(node.type);
const isTextualNode = (node) => isTextual(node.type);
const isEmptyParagraph = (node) => node.type === Node_Type.PARAGRAPH && node.nodes?.length === 0;
const isTextNode = (node) => node.type === Node_Type.TEXT;
const toId = (node) => node.id;
const toWords = (node) => extract(node).map(({ textData }) => textData?.text ?? "").get().flat().join("").split(" ").filter((w) => w !== "");
const toTextLength = (node) => extract(node).map(({ textData }) => textData?.text?.length ?? 0).get().flat().reduce((acc, l) => acc + l, 0);
const toWordCount = flow(toWords, A.size);
const toNodeWordCount = (node) => pipe([node, node], T.bimap(toWordCount, toId));
const generateNodeIds = (content) => {
  const generatedIds = /* @__PURE__ */ new Set();
  const modifiedContent = modify(content).filter((node) => !node.id && !isTextNode(node)).set((node) => {
    const generatedId = generateId();
    generatedIds.add(generatedId);
    return [{ ...node, id: generatedId }];
  });
  return [modifiedContent, generatedIds];
};
const restoreEmptyIds = (content, generatedIds) => modify(content).filter((node) => generatedIds.has(node.id)).set((node) => [{ ...node, id: "" }]);
const limitToCount = (count) => (nodeWordCount) => nodeWordCount.reduce(
  ([acc, wordLimit], [id, nodeCount]) => [
    [...acc, [id, Math.min(wordLimit, nodeCount)]],
    Math.max(wordLimit - nodeCount, 0)
  ],
  [[], count]
)[0];
const limitWords = (node, count) => {
  const wordCount = toWordCount(node);
  if (wordCount === count) {
    return node;
  }
  const truncatedLength = toWords(node).slice(0, count).join(" ").length;
  const nodeTextLengths = node.nodes?.filter(isTextNode).map(toTextLength) ?? [];
  const reducedLengths = nodeTextLengths.reduce(
    ([reducedLengths2, limit], nodeLength) => [[...reducedLengths2, Math.min(nodeLength, limit)], Math.max(limit - nodeLength, 0)],
    [[], truncatedLength]
  )[0];
  const shouldAppendEllipsis = (i) => reducedLengths[i] > 0 && reducedLengths[i] < nodeTextLengths[i] || reducedLengths[i] > 0 && reducedLengths[i + 1] === 0;
  const nodes = node.nodes?.filter(isTextNode).map((n, i) => {
    return {
      ...n,
      textData: {
        ...n.textData,
        decorations: n.textData?.decorations ?? [],
        text: n.textData?.text?.slice(0, reducedLengths[i]).concat(shouldAppendEllipsis(i) ? "…" : "") ?? ""
      }
    };
  }).filter((n) => n.textData?.text?.length) ?? [];
  return { ...node, nodes };
};
const defaultSelectCountableNodes = (node) => !isEmptyParagraph(node) && DEFAULT_COUNTABLE_TYPES.includes(node.type);
const applyToContent = (content) => (nodeWordCount) => modify(content).filter((n) => Object.keys(nodeWordCount).includes(n.id)).set((n) => nodeWordCount[n.id] === 0 ? [] : [limitWords(n, nodeWordCount[n.id])]);
const byNodeCount = (count, selectCountableNodes) => (content) => pipe(
  content,
  getFilteredNodes(selectCountableNodes),
  (nodes) => nodes.slice(count).map(toId),
  removeNodesById(content)
);
const byWordsCount = (count) => (content) => pipe(
  extract(content.nodes ?? []).filter(and([isTextualNode, not(isEmptyParagraph)])).get(),
  A.map(toNodeWordCount),
  limitToCount(count),
  fromEntries,
  applyToContent(content)
);
const removeNodesById = (content) => (ids) => modify(content).set((n) => ids.includes(n.id) ? [] : [n]);
const byNonTextNodesCount = (count) => (content) => pipe(
  extract(content?.nodes ?? []).filter(isNonTextualNode).map(toId).get().slice(count),
  removeNodesById(content)
);
const withFallback = (limits) => ({
  ...{
    nodeCount: Infinity,
    wordCount: Infinity,
    maxPlugins: Infinity
  },
  ...limits
});
const removeEmptyContainers = (content) => modify(content).filter(
  (n) => [
    Node_Type.LIST_ITEM,
    Node_Type.ORDERED_LIST,
    Node_Type.BULLETED_LIST,
    Node_Type.BLOCKQUOTE,
    Node_Type.TABLE,
    Node_Type.COLLAPSIBLE_LIST
  ].includes(n.type)
).set(
  (n) => n.nodes?.length === 0 ? [] : n.type === Node_Type.LIST_ITEM && n.nodes.every((nChild) => nChild.nodes.length === 0) ? [] : [n]
);
const removeEmptyCollapsibleListTitleAndBody = (content) => modify(content).filter(
  (n) => [Node_Type.COLLAPSIBLE_ITEM_TITLE, Node_Type.COLLAPSIBLE_ITEM_BODY].includes(n.type)
).set((n) => !n.nodes || n.nodes.length === 0 ? [] : [n]);
const removeEmptyCollapsibleListItems = (content) => modify(content).filter((n) => n.type === Node_Type.COLLAPSIBLE_ITEM).set((n) => {
  if (!n.nodes || n.nodes.length === 0) {
    return [];
  }
  const hasTitleAndBody = (node) => Boolean(
    node.nodes && node.nodes.some((child) => child.type === Node_Type.COLLAPSIBLE_ITEM_TITLE) && node.nodes.some((child) => child.type === Node_Type.COLLAPSIBLE_ITEM_BODY)
  );
  return !hasTitleAndBody(n) ? [] : [n];
});
const removeEmptyCollapsibleLists = (content) => modify(content).filter((n) => n.type === Node_Type.COLLAPSIBLE_LIST).set((n) => !n.nodes || n.nodes?.length === 0 ? [] : [n]);
const removeInvalidCollapsibleLists = (content) => pipe(
  content,
  removeEmptyCollapsibleListTitleAndBody,
  removeEmptyCollapsibleListItems,
  removeEmptyCollapsibleLists
);
const removeEmptyRows = (content) => modify(content).filter((n) => [Node_Type.TABLE_ROW, Node_Type.COLLAPSIBLE_ITEM].includes(n.type)).set((n) => n.nodes?.every((cell) => cell.nodes?.length === 0) ? [] : [n]);
const mergeEmptyParagraphs = (content) => pipe(
  content,
  getFilteredNodes(
    (node, { previousSibling }) => !!(isEmptyParagraph(node) && previousSibling && isEmptyParagraph(previousSibling))
  ),
  (nodes) => nodes.map(toId),
  removeNodesById(content)
);
const truncateRichContent = (content, limits, options) => {
  const { nodeCount, wordCount, maxPlugins } = withFallback(limits);
  const [contentWithIds, generatedIds] = generateNodeIds(content);
  const truncatedContent = pipe(
    contentWithIds,
    byNonTextNodesCount(maxPlugins),
    byNodeCount(nodeCount, options?.selectCountableNodes || defaultSelectCountableNodes),
    byWordsCount(wordCount),
    removeEmptyRows,
    removeEmptyContainers,
    removeInvalidCollapsibleLists,
    options?.keepEmptyParagraphs ? identity : mergeEmptyParagraphs
  );
  return restoreEmptyIds(truncatedContent, generatedIds);
};
export {
  getFilteredNodes,
  truncateRichContent
};
//# sourceMappingURL=ricos-truncate.js.map
