<!doctype html>

<html>
  <head>
    <link rel="stylesheet" href="lib/style.css">
    <script type="module"src="examples/parent-child.js"></script>
  </head>

  <body id="app">
    <h1>Hello Plunker!</h1>
  </body>
</html>
import Outlet from "./outlet.js";
import diffHTML from "./diffHTML.js";
import bindEvents from "./bindEvents.js";
export * from "./context.js";
import { context } from "./context.js";
import injectSlotContent from "./injectSlotContent.js";

let id = 0;

function normalizeSlots(props) {
const slots = Object.assign(props.slots || {})

 if (!Object.keys(slots).length && props.children) {
slots.default = props.children;
 }

 if(
   typeof slots === "function"||
    !Object.keys(slots).length) {
   slots['default'] = slots.default || slots;
 }
props.slots = slots
return props
}

function addDefaultDataSlotName(htmlString) {
  // Regex to match <slot> tags that DO NOT have a data-slot-name attribute
  const regex = /<slot(?![^>]*\sdata-slot-name="[^"]*")[^>]*>/gi;

  return htmlString.replace(regex, (match) => {
    // If the slot already has a closing slash (e.g., <slot />), ensure it's kept
    // We add data-slot-name="default" before the closing bracket
    return match.replace(/>|\/>/, (match) => {
return ` data-slot-name="default"${match}`
    });
  });
}

export function createComponent(
  renderFn,
  {
    state: initialState = {},
    on = {},
    onMount,
    onUnmount,
    onBeforeMount,
    onBeforeUnmount,
    onUpdate,
  } = {}
) {
  let el = null;
  let mounted = false;
  let props = {};
  let prevProps = {};
  let scheduledRenderProps = null;
  let onBeforeMountDone = false;
  let boundEvents = [];
  let state = initialState;
  let _cachedNode = null;
  let _renderedNull = false;
  const originalInitialState = { ...initialState };
  let renderScheduled = false;
  let contextUnsubs = [];

  const setState = (next) => {
    state = typeof next === "function" ? next(state) : { ...state, ...next };
    if (!renderScheduled) {
      renderScheduled = true;
      scheduledRenderProps = { ...props };
      queueMicrotask(() => {
        renderScheduled = false;
        componentFn(scheduledRenderProps);
        scheduledRenderProps = null;
      });
    }
  };

  const api = {
    refs: {},
    setState,
    props,

    mount(targetOrSelector, initialProps = {}) {
      if (mounted || onBeforeMountDone) return;

      let target =
        typeof targetOrSelector === "string"
          ? document.querySelector(targetOrSelector)
          : targetOrSelector;

      if (!target) throw new Error(`No element matches: ${targetOrSelector}`);

      const proceed = () => {
        onBeforeMountDone = true;
        el = target;

        props = { ...props, ...initialProps };
        prevProps = { ...props };
        api.props = props;

        componentFn(props);

        // ✅ Automatically hydrate component instances passed to props.children or props.slots
        normalizeSlots(props)

        Object.entries(props.slots).forEach(([key, value]) => {
          let ntarget = componentFn.refs[key];

          if (!ntarget || value == null) return;
          injectSlotContent(ntarget, value);
        });

        bindEvents(componentFn, el, on, boundEvents); // ✅ ensure this.el is hydrated before context

        if (!_renderedNull) {
          mounted = true;
          onMount?.call(componentFn);
          setupContextListeners();
          // syncInstanceToAPI(api, componentFn);
        }
      };

      runBeforeHook(onBeforeMount, proceed);
    },

    update(input = {}) {
      if (!mounted) return;
      const nextProps = input.props ? input.props : input;

      const newOn = nextProps.on;
      if (newOn && newOn !== on) {
        on = newOn;
      }

      const merged = { ...props, ...nextProps };
      prevProps = { ...props };
      props = merged;
      api.props = props;

      componentFn(props);
    },

    render(newProps = {}) {
      props = { ...props, ...newProps };
      api.props = props;
      let html =
        typeof renderFn === "function"
          ? renderFn.call(api, { state, setState, props, refs: api.refs })
          : renderFn;

   html =  addDefaultDataSlotName(html)

      html = Outlet(html, props);

      if (html) {
        html = new String(html);
        html._id = componentFn._id;
      }

      return html;
    },

    _resetInternal: () => {
      _cachedNode = null;
      _renderedNull = false;
      el = null; // <-- Add this line
      mounted = false;
      onBeforeMountDone = false;
      boundEvents = [];
      props = {};
      prevProps = {};
      state = { ...originalInitialState };
    },

    unmount() {
      if (!mounted || !el) return;

      const cleanup = () => {
        if (el.firstChild) {
          _cachedNode = el.firstChild.cloneNode(true);
          el.removeChild(el.firstChild);
        }

        boundEvents.forEach(({ node, type, listener }) => {
          node.removeEventListener(type, listener);
        });
        boundEvents = [];

        contextUnsubs.forEach((unsub) => unsub());
        contextUnsubs = [];

        mounted = false;
        onBeforeMountDone = false;
        _renderedNull = true;
        onUnmount?.call(api);

        if (api._resetInternal) api._resetInternal();
      };

      runBeforeHook(onBeforeUnmount, cleanup);
    },

    get renderFn() {
      return renderFn;
    },
    get el() {
      return el;
    },
    get state() {
      return state;
    },
  };

  api.ref = function (name) {
    if (!this.el) return null;
    return (
      this.el.querySelector(`[data-ref="${name}"]`) ||
      this.el.querySelector(`slot[data-slot-name="${name}"]`) ||
      this.el.querySelector(name) ||
      null
    );
  };

  api.refs = new Proxy(
    {},
    {
      get(_, key) {
        return api.ref(key);
      },
    }
  );

  api.action = function (e) {
    return e.target.closest("[data-action]")?.dataset.action || null;
  };

  function setupContextListeners() {
    contextUnsubs = [];
    Object.entries(on).forEach(([key, handler]) => {
      if (key.includes("::")) {
        const bound = handler.bind(api);
        const unsub = context.subscribe(key, bound);
        contextUnsubs.push(unsub);
      }
    });
  }

  function render(currentProps) {
    props = currentProps;
    api.props = props;

    const html = api.render(props);

    if (html === null || html === "") {
      if (!_renderedNull && el && el.firstChild) {
        const realNode = el.firstChild;
        _cachedNode = realNode.cloneNode(true);
        runBeforeHook(onBeforeUnmount, () => {
          // Remove all child nodes, not just first
          el.innerHTML = "";
          boundEvents.forEach(({ node, type, listener }) => {
            node.removeEventListener(type, listener);
          });
          boundEvents = [];
          mounted = false;
          _renderedNull = true;
          onUnmount?.call(api);
          // syncInstanceToAPI(api, componentFn)
        });
      } else {
        _renderedNull = true;
      }
      return;
    }

    if (_renderedNull && _cachedNode) {
      el.appendChild(_cachedNode);
      _cachedNode = null;
      _renderedNull = false;
      mounted = true;
      bindEvents(api, el, on, boundEvents);
      onMount?.call(api);
      // syncInstanceToAPI(api, componentFn);
      return;
    }

    _renderedNull = false;

    diffHTML(el, html);

    bindEvents(api, el, on, boundEvents);




    if (mounted && onUpdate) {
      onUpdate.call(api, prevProps);
    }
    // syncInstanceToAPI(api, componentFn);
  }

  function runBeforeHook(hook, next) {
    if (hook) {
      if (hook.length) hook.call(api, next);
      else Promise.resolve(hook.call(api)).then(next);
    } else {
      next();
    }
  }

  // Create the callable component function
  function componentFn(props = {}) {
    componentFn._render(props);
    // syncInstanceToAPI(api, componentFn);
    return componentFn;
  }

  // Attach API methods
  Object.assign(componentFn, api);

  // Dynamically attach instance-backed getters
  ["el", "props", "state", "setState"].forEach((key) => {
    Object.defineProperty(componentFn, key, {
      get() {
        return api[key];
      },
      enumerable: true,
    });
  });
  componentFn._render = render;
  componentFn.render = api.render;
  componentFn._id = id++;
  return componentFn;
}

export function renderList(
  array,
  renderFn,
  keyFn = (item) => item.id ?? item.key ?? item
) {
  return array
    .map((item, index) => {
      const key = keyFn(item, index);
      const inner = renderFn(item, index);
      // Only add data-key if inner is string and starts with a tag
      if (typeof inner === "string") {
        return inner.replace(
          /^<([a-zA-Z0-9-]+)/,
          `<$1 data-key="${String(key).replace(/"/g, "&quot;")}"`
        );
      }
      return inner;
    })
    .join("");
}
export default function bindEvents(api, el, on, boundEvents) {
  if (!el || !on) return;
  const root = el.firstElementChild;
  if (!root) return;

  Object.entries(on).forEach(([key, handler]) => {
    const isColonSyntax = key.includes(":");
    const [eventType, actionOrSelector] = isColonSyntax
      ? key.split(":")
      : key.split(" ");

    const isWildcardEvent = eventType === "*";
    const eventsToBind = isWildcardEvent
      ? ["click", "input", "change", "keydown", "submit"]
      : [eventType];

    eventsToBind.forEach((type) => {
      const listener = (e) => {
        const actionTarget = e.target.closest("[data-action]");
        const actionValue = actionTarget?.dataset.action || "";
        const dataArgsRaw = actionTarget?.dataset.args;

        const actionParts = actionValue.split(":");
        const actionName = actionParts[0];
        let actionArgs = [];

        // Prefer data-args if present
        if (dataArgsRaw) {
          try {
            const parsed = JSON.parse(dataArgsRaw);
            actionArgs = Array.isArray(parsed) ? parsed : [parsed];
          } catch (err) {
            console.warn("Invalid JSON in data-args:", dataArgsRaw);
          }
        } else {
          actionArgs = actionParts.slice(1);
        }

        const context = {
          event: e,
          state: api.state,
          setState: api.setState,
          props: api.props,
          refs: api.refs,
          action: actionName,
          args: actionArgs,
        };

        // Action handler
        if (isColonSyntax && actionTarget) {
          const matches =
            (actionOrSelector === "*" || actionOrSelector === actionName) &&
            (isWildcardEvent || e.type === eventType);

          if (matches) {
            return handler.call(api, context);
          }
        }

        // Fallback to selector-based if no action match
        if (!isColonSyntax) {
          const target = e.target.closest(actionOrSelector || "*");
          if (target && root.contains(target)) {
            return handler.call(api, context);
          }
        }
      };

      const marker = `__microBound_${type}_${key}`;
      if (!root[marker]) {
        root.addEventListener(type, listener);
        root[marker] = true;
        boundEvents.push({ node: root, type, listener });
      }
    });
  });
}
export function createState(initial) {
  let state = initial;
  const subs = new Set();

  function setState(next) {
    state = typeof next === "function" ? next(state) : { ...state, ...next };
    subs.forEach((fn) => fn(state));
  }

  function subscribe(fn) {
    subs.add(fn);
    fn(state);
    return () => subs.delete(fn);
  }

  function get() {
    return state;
  }

  return { get, setState, getState: get, subscribe };
}
// Top-level shared pub/sub
const channels = new Map();

export const context = {
  subscribe(eventName, fn) {
    if (!channels.has(eventName)) {
      channels.set(eventName, new Set());
    }
    const set = channels.get(eventName);
    set.add(fn);
    return () => set.delete(fn);
  },

  emit(eventName, payload) {
    const set = channels.get(eventName);
    if (set) {
      for (const fn of set) fn(payload);
    }
  },

  clear() {
    channels.clear(); // Optional: useful for testing or full reset
  },
};

const stores = new Map();

export function shared(key, initial = {}) {
  if (!stores.has(key)) {
    const state = createState(initial);
    const api = {
      ...state,
      emit(event, payload) {
        state.setState(payload);
        context.emit(`${key}::${event}`, state.getState());
      },
      on(event, fn) {
        return context.subscribe(`${key}::${event}`, fn);
      },
    };
    stores.set(key, api);
  }

  return stores.get(key);
}
shared.clear = () => stores.clear();
export default function diffHTML(el, newHTML) {
  if (!el) return false;

  // Optional: parse with cache using newHTML._id
  const temp = document.createElement("div");
  temp.innerHTML = newHTML;

    // Special case: if newHTML is plain text (no elements)
  if (temp.children.length === 0 && temp.textContent) {
    el.textContent = temp.textContent;
    return true;
  }

  const newChildren = Array.from(temp.children);
  const oldChildren = Array.from(el.children);

  const newKeyed = new Map();
  const oldKeyed = new Map();

  for (const child of newChildren) {
    const key = child.dataset.key;
    if (key) newKeyed.set(key, child);
  }

  for (const child of oldChildren) {
    const key = child.dataset.key;
    if (key) oldKeyed.set(key, child);
  }

  let cursor = 0;
  for (const newChild of newChildren) {
    const key = newChild.dataset.key;
    let currentNode = el.children[cursor];

    if (key && oldKeyed.has(key)) {
      const existing = oldKeyed.get(key);

      patchElement(existing, newChild);

      if (existing !== currentNode) {
        el.insertBefore(existing, currentNode || null);
      }

      oldKeyed.delete(key);
    } else {
      el.insertBefore(newChild, currentNode || null);
    }

    cursor++;
  }

  for (const leftover of oldKeyed.values()) {
    leftover.remove();
  }

  while (el.children.length > newChildren.length) {
    el.lastChild.remove();
  }

  return true;
}

function patchElement(fromEl, toEl) {
  if (fromEl.tagName !== toEl.tagName) {
    fromEl.replaceWith(toEl.cloneNode(true));
    return;
  }
  syncAttributes(fromEl, toEl);
  patchChildren(fromEl, toEl);
}

function syncAttributes(fromEl, toEl) {
  const fromAttrs = fromEl.attributes;
  const toAttrs = toEl.attributes;
  for (const { name } of Array.from(fromAttrs)) {
    if (!toEl.hasAttribute(name)) {
      fromEl.removeAttribute(name);
    }
  }

  for (const { name, value } of Array.from(toAttrs)) {
    if (fromEl.getAttribute(name) !== value) {
      fromEl.setAttribute(name, value);
    }
  }
}

function patchChildren(fromEl, toEl) {
  const fromNodes = Array.from(fromEl.childNodes);
  const toNodes = Array.from(toEl.childNodes);
  const max = Math.max(fromNodes.length, toNodes.length);

  for (let i = 0; i < max; i++) {
    const fromNode = fromNodes[i];
    const toNode = toNodes[i];

    if (!toNode && fromNode) {
      fromEl.removeChild(fromNode);
      continue;
    }

    if (!fromNode && toNode) {
      fromEl.appendChild(toNode.cloneNode(true));
      continue;
    }

    patchNode(fromNode, toNode);
  }
}

function patchNode(fromNode, toNode) {

  if (fromNode.isEqualNode(toNode)) return;
  if (fromNode.nodeType !== toNode.nodeType) {
    fromNode.replaceWith(toNode.cloneNode(true));
    return;
  }

  if (fromNode.nodeType === Node.TEXT_NODE) {
    if (fromNode.nodeValue !== toNode.nodeValue) {
      fromNode.nodeValue = toNode.nodeValue;
    }
  } else if (
    fromNode.nodeType === Node.ELEMENT_NODE &&
    fromNode.tagName === toNode.tagName
  ) {
    patchElement(fromNode, toNode);
  } else {
    fromNode.replaceWith(toNode.cloneNode(true));
  }
}
// injectSlotContent.js

export default function injectSlotContent(refNode, value) {
  if (!refNode || value == null) return;

  const resolveItem = (item) => (typeof item === "function" ? item() : item);

  const mountOrAppend = (item, target) => {
    if (item?.mount instanceof Function) {
      const temp = document.createElement("div");
      item.mount(temp);
      if (temp.firstElementChild) target.appendChild(temp.firstElementChild);
    } else if (item?.el instanceof HTMLElement) {
      target.appendChild(item.el);
    } else if (item instanceof Node) {
      target.appendChild(item);
    } else if (typeof item === "string") {
      const temp = document.createElement("div");
      temp.innerHTML = item;
      if (temp.firstElementChild) target.appendChild(temp.firstElementChild);
    }
  };

  const resolved = resolveItem(value);

  if (Array.isArray(resolved)) {
    const fragment = document.createDocumentFragment();
    resolved.forEach((item) => mountOrAppend(resolveItem(item), fragment));
    refNode.replaceWith(fragment);
    return;
  }

  // Single item
  const tempContainer = document.createElement("div");
  if (resolved?.mount instanceof Function) {
    refNode.replaceWith(tempContainer);
    resolved.mount(tempContainer);
  } else if (resolved?.el instanceof HTMLElement) {
    refNode.replaceWith(resolved.el);
  } else if (resolved instanceof Node) {
    refNode.replaceWith(resolved);
  } else if (typeof resolved === "string") {
    tempContainer.innerHTML = resolved;
    if (tempContainer.firstElementChild) {
      refNode.replaceWith(tempContainer.firstElementChild);
    }
  } else {
    refNode.replaceWith(document.createTextNode(""));
  }

}
export default function Outlet(html, props = {}) {
  if (typeof html !== "string") return ""; // return empty string if null or non-string

  const { children = {}, slots = {} } = props;

  function getSlotContent(name, fallback) {
    let value;

    if (!name) {
      if (
        children &&
        typeof children === "object" &&
        children.default != null
      ) {
        value = children.default;
      } else if (slots && typeof slots === "object" && slots.default != null) {
        value = slots.default;
      } else if (typeof children === "string") {
        value = children;
      } else {
        return fallback;
      }
    } else {
      value = children?.[name] ?? slots?.[name] ?? fallback;
    }

    if (typeof value === "function") {
      value = value();
    }

    if (value?.el instanceof HTMLElement) {
      return value.el.outerHTML; // insert HTML, real DOM will be handled later
    }

    if (value instanceof Node) {
      const temp = document.createElement("div");
      temp.appendChild(value.cloneNode(true));
      return temp.innerHTML;
    }

    return value ?? fallback;
  }

  html = html.replace(
    /<slot(?:\s+name="([^"]+)")?>([\s\S]*?)<\/slot>/gis,
    (_, name, fallback) => getSlotContent(name, fallback)
  );

  html = html.replace(
    /<([a-z]+)([^>]*)\sdata-slot="([^"]+)"([^>]*)>([\s\S]*?)<\/\1>/gis,
    (_, tag, before, name, after, fallback) => {
      const content = getSlotContent(name, fallback);
      return `<${tag}${before}${after}>${content}</${tag}>`;
    }
  );

  return html;
}
import {createComponent} from "../lib/reactive-core.js"


  const Child = createComponent(({state}) => {

  return `<div data-key="1"><p>I'm the child</p>
  <button>Count ${state.count ?? ""}</button></div>
  `
  },{
    state: {count: 0},
    on: {
      "click button"(){

        this.setState(prev => 
           ({count: ++prev.count})
        )
      }
    }
  });

  const Parent = createComponent(() => `
  <div class="card">
    <header data-slot-name="header"></header>
    <main><slot>DEfaulttttt</slot></main>
    <footer data-slot-name="footer">Default Footer</footer>
  </div>
  `);

let props = {
  slots: {
    //default: "<p>Hello world!</p>",
    footer: "<p>Custom footer here</p>"
  }
}

props =  {
    slots: Child(),
    //slots:{footer:  Child()},
     //children: Child(),
  }
 
  Parent.mount("#app",props);