import { getIn } from "immutable";
import { isElementReference } from "@makeswift/controls";
import * as Introspection from "../../prop-controllers/introspection";
import { ActionTypes, isKnownAction } from "../actions";
import { getRootElement } from "./read-only-documents";
function getInitialState(documents, descriptors) {
  const state = /* @__PURE__ */ new Map();
  if (documents == null || descriptors == null)
    return state;
  for (const [documentKey, document] of documents) {
    state.set(documentKey, buildElementTree(getRootElement(document), descriptors));
  }
  return state;
}
function getElementTree(state, documentKey) {
  return state.get(documentKey) ?? null;
}
function getElements(state, documentKey) {
  return getElementTree(state, documentKey)?.elements ?? /* @__PURE__ */ new Map();
}
function getElementIds(state, documentKey) {
  return getElementTree(state, documentKey)?.elementIds ?? /* @__PURE__ */ new Map();
}
function getElement(state, documentKey, elementKey) {
  return getElements(state, documentKey).get(elementKey) ?? null;
}
function getElementId(state, documentKey, elementKey) {
  return getElementIds(state, documentKey).get(elementKey) ?? null;
}
function reducer(state = getInitialState(), action) {
  if (!isKnownAction(action))
    return state;
  switch (action.type) {
    case ActionTypes.CREATE_ELEMENT_TREE: {
      const { document, descriptors } = action.payload;
      return new Map(state).set(
        document.key,
        buildElementTree(getRootElement(document), descriptors)
      );
    }
    case ActionTypes.DELETE_ELEMENT_TREE: {
      const nextState = new Map(state);
      const deleted = nextState.delete(action.payload.documentKey);
      return deleted ? nextState : state;
    }
    case ActionTypes.CHANGE_ELEMENT_TREE: {
      const { oldDocument, newDocument, descriptors, operation } = action.payload;
      const documentKey = oldDocument.key;
      console.assert(
        documentKey === newDocument.key,
        `Mismatching document keys ${documentKey} !== ${newDocument.key}`
      );
      const elementTree = state.get(documentKey);
      if (elementTree == null)
        return state;
      const updatedElementTree = applyChanges(
        elementTree,
        descriptors,
        {
          old: getRootElement(oldDocument),
          new: getRootElement(newDocument)
        },
        operation
      );
      return new Map(state).set(documentKey, updatedElementTree);
    }
    default:
      return state;
  }
}
function* traverseElementTree(element, descriptors) {
  yield element;
  if (isElementReference(element))
    return;
  const elementDescriptors = descriptors.get(element.type);
  if (elementDescriptors == null)
    return;
  for (const [propKey, descriptor] of Object.entries(elementDescriptors)) {
    const children = Introspection.getElementChildren(descriptor, element.props[propKey]);
    for (const child of children) {
      yield* traverseElementTree(child, descriptors);
    }
  }
}
function getElementIdProp(element, descriptors) {
  const elementDescriptors = descriptors.get(element.type);
  if (elementDescriptors == null)
    return null;
  for (const [propName, descriptor] of Object.entries(elementDescriptors)) {
    const elementId = Introspection.getElementId(descriptor, element.props[propName]);
    if (elementId != null)
      return elementId;
  }
  return null;
}
function buildElementTree(rootElement, descriptors) {
  const elements = /* @__PURE__ */ new Map();
  const elementIds = /* @__PURE__ */ new Map();
  for (const element of traverseElementTree(rootElement, descriptors)) {
    elements.set(element.key, element);
    if (!isElementReference(element)) {
      const elementId = getElementIdProp(element, descriptors);
      if (elementId != null)
        elementIds.set(element.key, elementId);
    }
  }
  return {
    elements,
    elementIds
  };
}
function isElement(item) {
  return typeof item === "object" && item != null && "key" in item && "type" in item && typeof item.key === "string" && typeof item.type === "string";
}
function getElementOperationPath(path) {
  if (path.length === 0) {
    return { elementPath: [], propName: null };
  }
  const i = path.findLastIndex(
    (fragment, i2) => typeof fragment === "number" && (i2 === path.length - 1 || path[i2 + 1] === "props")
  );
  if (i === -1 && path[0] !== "props" || path.length - i < 3) {
    console.error("Operation path does not point to an element property", { path });
    return { elementPath: [], propName: null };
  }
  return { elementPath: path.slice(0, i + 1), propName: `${path.slice(i + 1).at(1)}` };
}
function getChangedElementsPaths(path) {
  let elementOp = getElementOperationPath(path);
  const result = [elementOp];
  while (elementOp.elementPath.length > 0) {
    elementOp = getElementOperationPath(elementOp.elementPath.slice(0, -1));
    result.push(elementOp);
  }
  return result;
}
function getElementByPath(rootElement, elementPath) {
  const item = getIn(rootElement, elementPath);
  if (!isElement(item)) {
    console.error("Expected an element, got", item, {
      rootElement,
      elementPath
    });
    return null;
  }
  return item;
}
function getElementAndPropName(rootElement, { elementPath, propName }) {
  const element = getElementByPath(rootElement, elementPath);
  return element != null ? [element, propName] : [rootElement, null];
}
function updateParentElements(elements, elementPaths, rootElement) {
  elementPaths.forEach(({ elementPath }) => {
    const element = getElementByPath(rootElement, elementPath);
    if (element != null)
      elements.set(element.key, element);
  });
}
function hasChildren(element, propName, descriptors) {
  if (isElementReference(element))
    return false;
  const propDescriptor = descriptors.get(element.type)?.[propName];
  if (propDescriptor == null)
    return false;
  const children = Introspection.getElementChildren(propDescriptor, element.props[propName]);
  return children.length > 0;
}
function deleteElement({ elements, elementIds }, deletedElement, propName, descriptors) {
  if (propName == null || hasChildren(deletedElement, propName, descriptors)) {
    for (const element of traverseElementTree(deletedElement, descriptors)) {
      elements.delete(element.key);
      elementIds.delete(element.key);
    }
  } else {
    elements.delete(deletedElement.key);
    elementIds.delete(deletedElement.key);
  }
}
function applyDelete(elementTree, descriptors, rootElements, path) {
  const [deleteElementPath, ...parentElementPaths] = getChangedElementsPaths(path);
  const [deletedElement, propName] = getElementAndPropName(rootElements.old, deleteElementPath);
  const elements = new Map(elementTree.elements);
  const elementIds = new Map(elementTree.elementIds);
  deleteElement({ elements, elementIds }, deletedElement, propName, descriptors);
  updateParentElements(elements, parentElementPaths, rootElements.new);
  return {
    elements,
    elementIds
  };
}
function insertElement({ elements, elementIds }, insertedElement, propName, descriptors) {
  if (propName == null || hasChildren(insertedElement, propName, descriptors)) {
    for (const element of traverseElementTree(insertedElement, descriptors)) {
      elements.set(element.key, element);
      if (!isElementReference(element)) {
        const elementId = getElementIdProp(element, descriptors);
        if (elementId != null)
          elementIds.set(element.key, elementId);
      }
    }
  } else {
    elements.set(insertedElement.key, insertedElement);
    if (!isElementReference(insertedElement)) {
      const elementId = getElementIdProp(insertedElement, descriptors);
      if (elementId != null)
        elementIds.set(insertedElement.key, elementId);
    }
  }
}
function applyInsert(elementTree, descriptors, rootElements, path) {
  const [insertedElementPath, ...parentElementPaths] = getChangedElementsPaths(path);
  const [insertedElement, propName] = getElementAndPropName(rootElements.new, insertedElementPath);
  const elements = new Map(elementTree.elements);
  const elementIds = new Map(elementTree.elementIds);
  insertElement({ elements, elementIds }, insertedElement, propName, descriptors);
  updateParentElements(elements, parentElementPaths, rootElements.new);
  return {
    elements,
    elementIds
  };
}
function applyUpdate(elementTree, descriptors, rootElements, path) {
  const [updateElementPath, ...parentElementPaths] = getChangedElementsPaths(path);
  const [deletedElement, propName] = getElementAndPropName(rootElements.old, updateElementPath);
  const [insertedElement, _] = getElementAndPropName(rootElements.new, updateElementPath);
  const elements = new Map(elementTree.elements);
  const elementIds = new Map(elementTree.elementIds);
  deleteElement({ elements, elementIds }, deletedElement, propName, descriptors);
  insertElement({ elements, elementIds }, insertedElement, propName, descriptors);
  updateParentElements(elements, parentElementPaths, rootElements.new);
  return {
    elements,
    elementIds
  };
}
function applyChanges(elementTree, descriptors, rootElements, operation) {
  return operation.reduce((tree, op) => {
    const hasDelete = "ld" in op || "od" in op;
    const hasInsert = "li" in op || "oi" in op;
    if (hasDelete && hasInsert) {
      return applyUpdate(tree, descriptors, rootElements, op.p);
    }
    if (hasDelete)
      return applyDelete(tree, descriptors, rootElements, op.p);
    if (hasInsert)
      return applyInsert(tree, descriptors, rootElements, op.p);
    return tree;
  }, elementTree);
}
export {
  buildElementTree,
  getChangedElementsPaths,
  getElement,
  getElementId,
  getElementIds,
  getElements,
  getInitialState,
  reducer,
  traverseElementTree
};
//# sourceMappingURL=element-trees.js.map