console.log("[HOT RELOADING] Connected to server.");
function patch(json) {
  try {
    const views = JSON.parse(json);
    for (const [id, patches] of views) {
      console.log("[HOT RELOAD]", id, patches);
      const walker = document.createTreeWalker(
          document.body,
          NodeFilter.SHOW_COMMENT,
        ),
        open = `hot-reload|${id}|open`,
        close = `hot-reload|${id}|close`;
      let start, end;
      const instances = [];
      while (walker.nextNode()) {
        if (walker.currentNode.textContent == open) {
          start = walker.currentNode;
        } else if (walker.currentNode.textContent == close) {
          end = walker.currentNode;
          instances.push([start, end]);
          start = undefined;
          end = undefined;
        }
      }

      for (const [start, end] of instances) {
        for (const patch of patches) {
          const actualChildren = childrenFromRange(
            start.parentElement,
            start,
            end,
          );
          const child = childAtPath(
            actualChildren.length > 1
              ? { children: actualChildren }
              : actualChildren[0],
            patch.path,
          );
          const action = patch.action;
          if (action == "ClearChildren") {
            console.log("[HOT RELOAD] > ClearChildren", child.node);
            if (child.node) {
              child.node.textContent = "";
            } else {
              for (const existingChild of child.children) {
                let parent = existingChild.node.parentElement;
                parent.removeChild(existingChild.node);
              }
            }
          } else if (action.ReplaceWith) {
            console.log(
              "[HOT RELOAD] > ReplaceWith",
              child,
              action.ReplaceWith,
            );
            const replacement = fromReplacementNode(
              action.ReplaceWith,
              actualChildren,
            );
            if (child.node) {
              child.node.replaceWith(replacement);
            } else {
              if (child.children) {
                child.children[0].node.parentElement.insertBefore(
                  replacement,
                  child.children[0].node,
                );
                for (const existingChild of child.children) {
                  existingChild.node.parentElement.removeChild(
                    existingChild.node,
                  );
                }
              }
            }
          } else if (action.ChangeTagName) {
            const oldNode = child.node;
            console.log(
              "[HOT RELOAD] > ChangeTagName",
              child.node,
              action.ChangeTagName,
            );
            const newElement = document.createElement(action.ChangeTagName);
            for (const attr of oldNode.attributes) {
              newElement.setAttribute(attr.name, attr.value);
            }
            for (const childNode of child.node.childNodes) {
              newElement.appendChild(childNode);
            }

            child.node.replaceWith(newElement);
          } else if (action.RemoveAttribute) {
            console.log(
              "[HOT RELOAD] > RemoveAttribute",
              child.node,
              action.RemoveAttribute,
            );
            child.node.removeAttribute(action.RemoveAttribute);
          } else if (action.SetAttribute) {
            const [name, value] = action.SetAttribute;
            console.log(
              "[HOT RELOAD] > SetAttribute",
              child.node,
              action.SetAttribute,
            );
            child.node.setAttribute(name, value);
          } else if (action.SetText) {
            const node = child.node;
            console.log("[HOT RELOAD] > SetText", child.node, action.SetText);
            node.textContent = action.SetText;
          } else if (action.AppendChildren) {
            console.log(
              "[HOT RELOAD] > AppendChildren",
              child.node,
              action.AppendChildren,
            );
            const newChildren = action.AppendChildren.map((x) =>
              fromReplacementNode(x, actualChildren),
            );
            child.node.append(...newChildren);
          } else if (action.RemoveChild) {
            console.log(
              "[HOT RELOAD] > RemoveChild",
              child.node,
              child.children,
              action.RemoveChild,
            );
            const toRemove = child.children[action.RemoveChild.at];
            let toRemoveNode = toRemove.node;
            if (!toRemoveNode) {
              const range = new Range();
              range.setStartBefore(toRemove.start);
              range.setEndAfter(toRemove.end);
              toRemoveNode = range.deleteContents();
            } else {
              toRemoveNode.parentNode.removeChild(toRemoveNode);
            }
          } else if (action.InsertChild) {
            const newChild = fromReplacementNode(
              action.InsertChild.child,
              actualChildren,
            );
            let children = [];
            if (child.children) {
              children = child.children;
            } else if (child.start && child.end) {
              children = childrenFromRange(
                child.node || child.start.parentElement,
                start,
                end,
              );
            } else {
              console.warn("InsertChildAfter could not build children.");
            }
            const beforeNode = children[action.InsertChild.before];
            console.log(
              "[HOT RELOAD] > InsertChild",
              child,
              child.node,
              action.InsertChild,
              " before ",
              beforeNode,
            );
            if (beforeNode) {
              let node = beforeNode.node || beforeNode.start.previousSibling;
              node.parentElement.insertBefore(newChild, node);
            } else if (child.node) {
              child.node.appendChild(newChild);
            } else if (children) {
              let lastNode = children[children.length - 1];
              let afterNode = lastNode.node || lastNode.end.nextSibling;
              afterNode.after(newChild);
            }
          } else if (action.InsertChildAfter) {
            const newChild = fromReplacementNode(
              action.InsertChildAfter.child,
              actualChildren,
            );
            let children = [];
            if (child.children) {
              children = child.children;
            } else if (child.start && child.end) {
              children = childrenFromRange(
                child.node || child.start.parentElement,
                start,
                end,
              );
            } else {
              console.warn("InsertChildAfter could not build children.");
            }
            const after = children[action.InsertChildAfter.after];
            console.log(
              "[HOT RELOAD] > InsertChildAfter",
              child,
              child.node,
              action.InsertChildAfter,
              " after ",
              after,
            );
            if (
              child.node &&
              (!after || !(after.node || after.start).nextSibling)
            ) {
              child.node.appendChild(newChild);
            } else {
              const node = child.node || child.end;
              const parent =
                node.nodeType === Node.COMMENT_NODE ? node.parentNode : node;
              if (!after) {
                parent.appendChild(newChild);
              } else {
                parent.insertBefore(
                  newChild,
                  (after.node || after.start).nextSibling,
                );
              }
            }
          } else {
            console.warn("[HOT RELOADING] Unmatched action", action);
          }
        }
      }
    }
  } catch (e) {
    console.warn("[HOT RELOADING] Error: ", e);
  }

  function fromReplacementNode(node, actualChildren) {
    if (node.Html) {
      return fromHTML(node.Html);
    } else if (node.Fragment) {
      const frag = document.createDocumentFragment();
      for (const child of node.Fragment) {
        frag.appendChild(fromReplacementNode(child, actualChildren));
      }
      return frag;
    } else if (node.Element) {
      const element = document.createElement(node.Element.name);
      for (const [name, value] of node.Element.attrs) {
        element.setAttribute(name, value);
      }
      for (const child of node.Element.children) {
        element.appendChild(fromReplacementNode(child, actualChildren));
      }
      return element;
    } else {
      const child = childAtPath(
        actualChildren.length > 1
          ? { children: actualChildren }
          : actualChildren[0],
        node.Path,
      );
      if (child) {
        let childNode = child.node;
        if (!childNode) {
          const range = new Range();
          range.setStartBefore(child.start);
          range.setEndAfter(child.end);
          // okay this is somewhat silly
          // if we do cloneContents() here to return it,
          // we strip away the event listeners
          // if we're moving just one object, this is less than ideal
          // so I'm actually going to *extract* them, then clone and reinsert
          /* const toReinsert = range.cloneContents();
					if (child.end.nextSibling) {
						child.end.parentNode.insertBefore(toReinsert, child.end.nextSibling);
					} else {
						child.end.parentNode.appendChild(toReinsert);
					} */
          childNode = range.cloneContents();
        }
        return childNode;
      } else {
        console.warn(
          "[HOT RELOADING] Could not find replacement node at ",
          node.Path,
        );
        return undefined;
      }
    }
  }

  function buildActualChildren(element, range) {
    const walker = document.createTreeWalker(
      element,
      NodeFilter.SHOW_ELEMENT | NodeFilter.SHOW_TEXT | NodeFilter.SHOW_COMMENT,
      {
        acceptNode(node) {
          if (
            node.parentNode == element &&
            (!range || range.isPointInRange(node, 0))
          ) {
            return NodeFilter.FILTER_ACCEPT;
          } else {
            return NodeFilter.FILTER_REJECT;
          }
        },
      },
    );
    const actualChildren = [],
      elementCount = {};
    while (walker.nextNode()) {
      if (walker.currentNode.nodeType == Node.ELEMENT_NODE) {
        if (elementCount[walker.currentNode.nodeName]) {
          elementCount[walker.currentNode.nodeName] += 1;
        } else {
          elementCount[walker.currentNode.nodeName] = 0;
        }
        elementCount[walker.currentNode.nodeName];

        actualChildren.push({
          type: "element",
          name: walker.currentNode.nodeName,
          number: elementCount[walker.currentNode.nodeName],
          node: walker.currentNode,
          children: buildActualChildren(walker.currentNode),
        });
      } else if (walker.currentNode.nodeType == Node.TEXT_NODE) {
        actualChildren.push({
          type: "text",
          node: walker.currentNode,
        });
      } else if (walker.currentNode.nodeType == Node.COMMENT_NODE) {
        if (walker.currentNode.textContent.trim().startsWith("hot-reload|")) {
          if (walker.currentNode.textContent.trim().endsWith("|open")) {
            const startingName = walker.currentNode.textContent.trim();
            const componentName = startingName
              .replace("|open", "")
              .replace("hot-reload|", "");
            const endingName = `hot-reload|${componentName}|close`;
            let start = walker.currentNode;
            let depth = 1;

            while (walker.nextNode()) {
              if (walker.currentNode.textContent.trim() == endingName) {
                depth--;
              } else if (
                walker.currentNode.textContent.trim() == startingName
              ) {
                depth++;
              }

              if (depth == 0) {
                break;
              }
            }
            let end = walker.currentNode;
            actualChildren.push({
              type: "fragment",
              start: start.nextSibling,
              end: end.previousSibling,
              children: childrenFromRange(
                start.parentElement,
                start.nextSibling,
                end.previousSibling,
              ),
            });
          }
        } else if (walker.currentNode.textContent.trim() == "<() />") {
          actualChildren.push({
            type: "unit",
            node: walker.currentNode,
          });
        } else if (walker.currentNode.textContent.trim() == "<DynChild>") {
          let start = walker.currentNode;
          let depth = 1;

          while (walker.nextNode()) {
            if (walker.currentNode.textContent.trim() == "</DynChild>") {
              depth--;
            } else if (walker.currentNode.textContent.trim() == "<DynChild>") {
              depth++;
            }

            if (depth == 0) {
              break;
            }
          }
          let end = walker.currentNode;
          actualChildren.push({
            type: "dyn-child",
            start,
            end,
          });
        } else if (walker.currentNode.textContent.trim() == "<>") {
          let start = walker.currentNode;
          let depth = 1;

          while (walker.nextNode()) {
            if (walker.currentNode.textContent.trim() == "</>") {
              depth--;
            } else if (walker.currentNode.textContent.trim() == "<>") {
              depth++;
            }

            if (depth == 0) {
              break;
            }
          }
          let end = walker.currentNode;
          actualChildren.push({
            type: "fragment",
            children: childrenFromRange(start.parentElement, start, end),
            start,
            end,
          });
        } else if (walker.currentNode.textContent.trim().startsWith("<")) {
          let componentName = walker.currentNode.textContent.trim();
          let endMarker = componentName.replace("<", "</");
          let depth = 1;
          let start = walker.currentNode;
          while (walker.nextNode()) {
            if (walker.currentNode.textContent.trim() == endMarker) {
              depth--;
            } else if (walker.currentNode.textContent.trim() == componentName) {
              depth++;
            }

            if (depth == 0) {
              break;
            }
          }
          let end = walker.currentNode;
          actualChildren.push({
            type: "component",
            start,
            end,
          });
        }
      } else {
        console.warn(
          "[HOT RELOADING] Building children, encountered",
          walker.currentNode,
        );
      }
    }
    return actualChildren;
  }

  function childAtPath(element, path) {
    if (path.length == 0) {
      return element;
    } else if (element.children) {
      const next = element.children[path[0]],
        rest = path.slice(1);
      return childAtPath(next, rest);
    } else if (path == [0]) {
      return element;
    } else if (element.start && element.end) {
      const actualChildren = childrenFromRange(
        element.node || element.start.parentElement,
        element.start,
        element.end,
      );
      return childAtPath({ children: actualChildren }, path);
    } else {
      console.warn("[HOT RELOADING] Child at ", path, "not found in ", element);
      return element;
    }
  }

  function childrenFromRange(parent, start, end) {
    const range = new Range();
    range.setStartAfter(start);
    range.setEndBefore(end);
    return buildActualChildren(parent, range);
  }

  function fromHTML(html) {
    const template = document.createElement("template");
    template.innerHTML = html;
    return template.content.cloneNode(true);
  }
}
