import { h, Fragment } from 'preact';
import { getAttributeObject, selfClosingTags, getPropKey } from "./shared";
/**
 * Takes a template element and converts its content into a Preact VNode and also
 * extracts slot-element information.
 */
export function templateToPreactVNode(template) {
  const slots = {};
  const childVNodes = [];
  for (const childElement of template.content.children) {
    const {
      vnode,
      slots: childSlots
    } = nodeToPreactVNode(childElement, slots);
    Object.assign(slots, childSlots);
    childVNodes.push(vnode);
  }
  const vnode = h(Fragment, {}, childVNodes);
  return {
    vnode,
    slots
  };
}

/**
 * Recursively converts DOM nodes into Preact VNodes (virtual nodes) and also extracts
 * slot-element information.
 */
export function nodeToPreactVNode(node) {
  let slots = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
  let insideNestedComponent = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false;
  if (node.nodeType === 3) {
    return {
      vnode: node.textContent || '',
      slots
    };
  }
  if (node.nodeType !== 1) {
    return {
      vnode: null,
      slots
    };
  }
  const nodeName = String(node.nodeName).toLowerCase();
  if (nodeName === 'template') {
    // If we don't do this, Preact will clobber the information inside this template.
    // I've tried parsing the template.content here, but it didn't work, and that
    // would have been unnecessary processing anyway.
    const templateProps = {
      dangerouslySetInnerHTML: {
        __html: node.innerHTML
      }
    };
    return {
      vnode: h(nodeName, templateProps),
      slots
    };
  }
  if (nodeName.includes('-')) {
    // It's a custom element, we don't want to steal any of its slots.
    insideNestedComponent = true;
  }
  const childNodes = Array.from(node.childNodes);
  const children = [];
  for (const childNode of childNodes) {
    const {
      vnode,
      slots: childSlots
    } = nodeToPreactVNode(childNode, slots, insideNestedComponent);
    Object.assign(slots, childSlots);
    children.push(vnode);
  }
  const {
    slot,
    ...props
  } = getAttributeObject(node.attributes);
  if (nodeName === 'script') {
    return {
      vnode: null,
      slots
    };
  }
  if (nodeName === 'body') {
    return {
      vnode: h(Fragment, {}, children),
      slots
    };
  }
  if (selfClosingTags.includes(nodeName)) {
    return {
      vnode: h(nodeName, props),
      slots
    };
  }
  if (slot) {
    if (insideNestedComponent) {
      // Leave the element alone and put its slot back!
      return {
        vnode: h(nodeName, {
          slot,
          ...props
        }, children),
        slots
      };
    } else {
      return {
        vnode: null,
        slots: {
          ...slots,
          [getPropKey(slot)]: getSlotChildren(children)
        }
      };
    }
  }
  return {
    vnode: h(nodeName, props, children),
    slots
  };
}
function getSlotChildren(children) {
  const isString = item => typeof item === 'string';
  if (children.every(isString)) {
    return children.join(' ');
  }
  return h(Fragment, {}, children);
}
export function createSlotVNode(name) {
  return h('slot', {
    name
  });
}
export function getSlotNames(element) {
  return Array.from(element.childNodes).flatMap(getSlotNamesFromNode);
}
function getSlotNamesFromNode(node) {
  if (node.nodeType !== Node.ELEMENT_NODE) {
    return [];
  }
  const nodeName = String(node.nodeName).toLowerCase();
  if (nodeName === 'template') {
    return [];
  }
  const slot = node.getAttribute('slot');

  // Only recurse into child nodes if this is not a custom element, because we don't care
  // what's inside that nested custom element and don't want to steal its slots.
  const isCustomElement = nodeName.includes('-');
  if (!isCustomElement) {
    // If a named slot is nested inside other elements, the browser will interpret that
    // element as being part of the default slot content. In order for the browser to be
    // able to link the rendered `<slot name={name}>` element to the element provided by
    // the application (`<something slot="{name}">`), that provided element must be at
    // the root of the custom element. Therefore, if we find named slots nested in other
    // elements, we should warn application developers so they can fix it.
    const slotsFromChildren = Array.from(node.childNodes).flatMap(getSlotNamesFromNode);
    if (slotsFromChildren.length) {
      console.error('Named slots must be defined at the root level of your web component in order for the browser to find them. Found the following named slots incorrectly nested within other elements:', slotsFromChildren);
    }
  }
  if (slot) {
    return [slot];
  } else {
    return [];
  }
}