import { TagsWithInnerContent, computeHashes, hashTag, HasElementTags, tagDedupeKey, defineHeadPlugin } from '@unhead/shared';

function setAttrs(ctx, newEntry = false, markSideEffect) {
  const { tag, $el } = ctx;
  if (!$el)
    return;
  Object.entries(tag.props).forEach(([k, value]) => {
    value = String(value);
    const attrSdeKey = `attr:${k}`;
    if (k === "class") {
      if (!value)
        return;
      for (const c of value.split(" ")) {
        const classSdeKey = `${attrSdeKey}:${c}`;
        if (markSideEffect)
          markSideEffect(ctx, classSdeKey, () => $el.classList.remove(c));
        if (!$el.classList.contains(c))
          $el.classList.add(c);
      }
      return;
    }
    if (markSideEffect && !k.startsWith("data-h-"))
      markSideEffect(ctx, attrSdeKey, () => $el.removeAttribute(k));
    if (newEntry || $el.getAttribute(k) !== value)
      $el.setAttribute(k, value);
  });
  if (TagsWithInnerContent.includes(tag.tag)) {
    if (tag.textContent && tag.textContent !== $el.textContent)
      $el.textContent = tag.textContent;
    else if (tag.innerHTML && tag.innerHTML !== $el.innerHTML)
      $el.innerHTML = tag.innerHTML;
  }
}

let prevHash = false;
async function renderDOMHead(head, options = {}) {
  const beforeRenderCtx = { shouldRender: true };
  await head.hooks.callHook("dom:beforeRender", beforeRenderCtx);
  if (!beforeRenderCtx.shouldRender)
    return;
  const dom = options.document || head.resolvedOptions.document || window.document;
  const tagContexts = (await head.resolveTags()).map(setupTagRenderCtx);
  if (head.resolvedOptions.experimentalHashHydration) {
    prevHash = prevHash || head._hash || false;
    if (prevHash) {
      const hash = computeHashes(tagContexts.map((ctx) => ctx.tag._h));
      if (prevHash === hash)
        return;
      prevHash = hash;
    }
  }
  const staleSideEffects = head._popSideEffectQueue();
  head.headEntries().map((entry) => entry._sde).forEach((sde) => {
    Object.entries(sde).forEach(([key, fn]) => {
      staleSideEffects[key] = fn;
    });
  });
  const markSideEffect = (ctx, key, fn) => {
    key = `${ctx.renderId}:${key}`;
    if (ctx.entry)
      ctx.entry._sde[key] = fn;
    delete staleSideEffects[key];
  };
  function setupTagRenderCtx(tag) {
    const entry = head.headEntries().find((e) => e._i === tag._e);
    const renderCtx = {
      renderId: tag._d || hashTag(tag),
      $el: null,
      shouldRender: true,
      tag,
      entry,
      markSideEffect: (key, fn) => markSideEffect(renderCtx, key, fn)
    };
    return renderCtx;
  }
  const renders = [];
  const pendingRenders = {
    body: [],
    head: []
  };
  const markEl = (ctx) => {
    head._elMap[ctx.renderId] = ctx.$el;
    renders.push(ctx);
    markSideEffect(ctx, "el", () => {
      ctx.$el?.remove();
      delete head._elMap[ctx.renderId];
    });
  };
  for (const ctx of tagContexts) {
    await head.hooks.callHook("dom:beforeRenderTag", ctx);
    if (!ctx.shouldRender)
      continue;
    const { tag } = ctx;
    if (tag.tag === "title") {
      dom.title = tag.textContent || "";
      renders.push(ctx);
      continue;
    }
    if (tag.tag === "htmlAttrs" || tag.tag === "bodyAttrs") {
      ctx.$el = dom[tag.tag === "htmlAttrs" ? "documentElement" : "body"];
      setAttrs(ctx, false, markSideEffect);
      renders.push(ctx);
      continue;
    }
    ctx.$el = head._elMap[ctx.renderId];
    if (!ctx.$el && tag.key)
      ctx.$el = dom.querySelector(`${tag.tagPosition?.startsWith("body") ? "body" : "head"} > ${tag.tag}[data-h-${tag._h}]`);
    if (ctx.$el) {
      if (ctx.tag._d)
        setAttrs(ctx);
      markEl(ctx);
      continue;
    }
    pendingRenders[tag.tagPosition?.startsWith("body") ? "body" : "head"].push(ctx);
  }
  const fragments = {
    bodyClose: void 0,
    bodyOpen: void 0,
    head: void 0
  };
  Object.entries(pendingRenders).forEach(([pos, queue]) => {
    if (!queue.length)
      return;
    const children = dom?.[pos]?.children;
    if (!children)
      return;
    for (const $el of [...children].reverse()) {
      const elTag = $el.tagName.toLowerCase();
      if (!HasElementTags.includes(elTag))
        continue;
      const props = $el.getAttributeNames().reduce((props2, name) => ({ ...props2, [name]: $el.getAttribute(name) }), {});
      const tmpTag = { tag: elTag, props };
      if ($el.innerHTML)
        tmpTag.innerHTML = $el.innerHTML;
      const tmpRenderId = hashTag(tmpTag);
      let matchIdx = queue.findIndex((ctx) => ctx?.renderId === tmpRenderId);
      if (matchIdx === -1) {
        const tmpDedupeKey = tagDedupeKey(tmpTag);
        matchIdx = queue.findIndex((ctx) => ctx?.tag._d && ctx.tag._d === tmpDedupeKey);
      }
      if (matchIdx !== -1) {
        const ctx = queue[matchIdx];
        ctx.$el = $el;
        setAttrs(ctx);
        markEl(ctx);
        delete queue[matchIdx];
      }
    }
    queue.forEach((ctx) => {
      const pos2 = ctx.tag.tagPosition || "head";
      fragments[pos2] = fragments[pos2] || dom.createDocumentFragment();
      if (!ctx.$el) {
        ctx.$el = dom.createElement(ctx.tag.tag);
        setAttrs(ctx, true);
      }
      fragments[pos2].appendChild(ctx.$el);
      markEl(ctx);
    });
  });
  if (fragments.head)
    dom.head.appendChild(fragments.head);
  if (fragments.bodyOpen)
    dom.body.insertBefore(fragments.bodyOpen, dom.body.firstChild);
  if (fragments.bodyClose)
    dom.body.appendChild(fragments.bodyClose);
  for (const ctx of renders)
    await head.hooks.callHook("dom:renderTag", ctx);
  Object.values(staleSideEffects).forEach((fn) => fn());
}
let domUpdatePromise = null;
async function debouncedRenderDOMHead(head, options = {}) {
  function doDomUpdate() {
    domUpdatePromise = null;
    return renderDOMHead(head, options);
  }
  const delayFn = options.delayFn || ((fn) => setTimeout(fn, 10));
  return domUpdatePromise = domUpdatePromise || new Promise((resolve) => delayFn(() => resolve(doDomUpdate())));
}

function PatchDomOnEntryUpdatesPlugin(options) {
  return defineHeadPlugin({
    hooks: {
      "entries:updated": function(head) {
        if (typeof options?.document === "undefined" && typeof window === "undefined")
          return;
        let delayFn = options?.delayFn;
        if (!delayFn && typeof requestAnimationFrame !== "undefined")
          delayFn = requestAnimationFrame;
        debouncedRenderDOMHead(head, { document: options?.document || window.document, delayFn });
      }
    }
  });
}

function maybeGetSSRHash(document) {
  return document?.head.querySelector('meta[name="unhead:ssr"]')?.getAttribute("content") || false;
}

export { PatchDomOnEntryUpdatesPlugin, debouncedRenderDOMHead, domUpdatePromise, maybeGetSSRHash, renderDOMHead, setAttrs };
