function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

function _defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } }

function _createClass(Constructor, protoProps, staticProps) { if (protoProps) _defineProperties(Constructor.prototype, protoProps); if (staticProps) _defineProperties(Constructor, staticProps); return Constructor; }

function ownKeys(object, enumerableOnly) { var keys = Object.keys(object); if (Object.getOwnPropertySymbols) { var symbols = Object.getOwnPropertySymbols(object); if (enumerableOnly) { symbols = symbols.filter(function (sym) { return Object.getOwnPropertyDescriptor(object, sym).enumerable; }); } keys.push.apply(keys, symbols); } return keys; }

function _objectSpread(target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i] != null ? arguments[i] : {}; if (i % 2) { ownKeys(Object(source), true).forEach(function (key) { _defineProperty(target, key, source[key]); }); } else if (Object.getOwnPropertyDescriptors) { Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); } else { ownKeys(Object(source)).forEach(function (key) { Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key)); }); } } return target; }

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

import { EMPTY, ComponentTreeCache, InstanceCache, Props, Transaction, VTree, State, ActiveRenderState } from "./util/types.js";
import { $$render, $$vTree, $$unsubscribe, $$type, $$hooks, $$insertAfter } from "./util/symbols.js";
import diff from "./util/binding.js";
import middleware from "./middleware.js";
const {
  outerHTML,
  innerHTML,
  createTree,
  release,
  Internals
} = diff;
const {
  isArray
} = Array;
const {
  setPrototypeOf,
  defineProperty,
  keys,
  assign
} = Object;
const RenderDebounce = new WeakMap();
/**
 * @param {Props} defaultProps
 * @returns {string[]}
 */

const getObserved = ({
  defaultProps
}) => defaultProps ? keys(defaultProps) : EMPTY.ARR;
/**
 * Creates the `component.props` object.
 *
 * @param {any} domNode
 * @param {Props} existingProps
 */


const createProps = (domNode, existingProps = {}) => {
  return getObserved(domNode.constructor).reduce((props, attr) => _objectSpread({
    [attr]: (domNode.hasAttribute(attr) ? domNode.getAttribute(attr) : domNode[attr]) || props[attr] || domNode.constructor.defaultProps[attr]
  }, props), existingProps);
};
/**
 * Creates the `component.state` object.
 *
 * @param {Component} domNode
 * @param {State} newState
 */


const createState = (domNode, newState) => assign({}, domNode.state, newState);
/**
 * Finds all VTrees associated with a component.
 *
 * @param {VTree[]} childTrees
 * @param {VTree} vTree
 */


const getChildTrees = (childTrees, vTree) => {
  ComponentTreeCache.forEach((parentTree, childTree) => {
    if (parentTree === vTree) {
      ComponentTreeCache.delete(childTree);

      if (typeof childTree.rawNodeName !== 'function') {
        childTrees.push(childTree);
      } else {
        getChildTrees(childTrees, childTree);
      }
    }
  });
};
/**
 * Represents a vanilla JavaScript Component. This is a lightweight version of
 * what you'd find in most modern web application libraries. It provides a
 * React-inspired API.
 */


let Component = /*#__PURE__*/function () {
  /**
   * Creates a lightweight stateful JavaScript component. Takes the static
   * default props and applies them to the incoming initial props.
   *
   * @param {Props} initialProps
   */
  function Component(initialProps) {
    _classCallCheck(this, Component);

    this.state = {};
    this[$$type] = 'class';
    this[$$vTree] = null;
    this[$$hooks] = [];
    let instance = this; // This is how we detect if this is a Web Component or not. The class
    // must be registered ahead of time within the customElements global
    // registry. This will allow us to construct the HTMLElement and attach the
    // shadow root.

    try {
      instance = Reflect.construct(HTMLElement, [], new.target);
      /** @type {any} */

      instance.attachShadow({
        mode: 'open'
      });
      /** @type {any} */

      instance[$$type] = 'web';
    } catch (e) {
      // Not a Web Component.

      /** @type {any} */
      instance[$$type] = 'class';
    }

    const props = instance.props = assign({}, initialProps);
    const {
      defaultProps = EMPTY.OBJ
    } =
    /** @type {any} */
    instance.constructor; // Merge default props into props object.

    if (typeof defaultProps === 'object' && !isArray(defaultProps)) {
      keys(defaultProps || EMPTY.OBJ).forEach(prop => {
        if (prop in props && props[prop] !== undefined) {
          return;
        }

        instance.props[prop] = defaultProps[prop];
      });
    }

    return instance;
  }
  /**
   * @param {Props} props
   * @param {State} state
   *
   * @returns {VTree[] | VTree | undefined}
   */
  // @ts-ignore


  _createClass(Component, [{
    key: "render",
    value: function render(props, state) {
      return undefined;
    }
    /**
     * Changes the component state synchronously by shallow merging the incoming
     * state into the previous state. Rendering will then be scheduled to the
     * next reachable tick. If this is called multiple times on a single tick
     * all that will happen is the state will be updated by no rendering will
     * occur until the next tick.
     *
     * You can await this method to know when rendering has completed.
     *
     * @param {State} state
     */

  }, {
    key: "setState",
    value: function setState(state) {
      this.state = assign({}, this.state, state);

      if (!RenderDebounce.has(this)) {
        RenderDebounce.set(this, new Promise(resolve => setTimeout(() => {
          RenderDebounce.delete(this);
          this.componentWillReceiveProps(this.props, this.state);

          if (this.shouldComponentUpdate(this.props, this.state)) {
            this[$$render]()?.then(resolve);
          } else {
            resolve(null);
          }
        })));
      }

      return RenderDebounce.get(this);
    }
    /**
     * Schedule a render to happen, bypassing shouldComponentUpdate. Bound to the
     * same tick logic as setState, allowing this to be called as many times as
     * you want.
     */

  }, {
    key: "forceUpdate",
    value: function forceUpdate() {
      if (!RenderDebounce.has(this)) {
        RenderDebounce.set(this, new Promise(resolve => setTimeout(() => {
          RenderDebounce.delete(this);
          this.componentWillReceiveProps(this.props, this.state);
          this[$$render]()?.then(resolve);
        })));
      }

      return RenderDebounce.get(this);
    }
    /** @type {State} */

  }, {
    key: $$render,
    value:
    /**
     * Stateful render. Used when a component changes and needs to re-render
     * itself. This is triggered on `setState` and `forceUpdate` calls.
     *
     * @return {Promise<Transaction> | undefined}
     */
    function () {
      // This is a WebComponent, so do something different.
      if (this[$$type] === 'web') {
        const oldProps = this.props;
        const oldState = this.state;
        this.props = createProps(this, this.props);
        this.state = createState(this, this.state);
        ActiveRenderState.push(this);
        /** @type {Promise<Transaction>} */

        const promise = innerHTML(
        /** @type {any} */
        this.shadowRoot, this.render(this.props, this.state));
        ActiveRenderState.length = 0;
        this.componentDidUpdate(oldProps, oldState);
        return promise;
      } // Get the fragment tree associated with this component. This is used to
      // lookup rendered children.


      let vTree = this[$$vTree];
      /**
       * Find all previously rendered top-level children associated to this
       * component. This will be used to diff against the newly rendered
       * elements.
       *
       * @type {VTree[]}
       */

      const childTrees = []; // Lookup all DOM nodes that were associated at the top level with this
      // component.

      vTree && getChildTrees(childTrees, vTree); // Render directly from the Component.

      ActiveRenderState.push(this);
      let renderTree = this.render(this.props, this.state);
      ActiveRenderState.length = 0; // Do not render.

      if (!renderTree) {
        return;
      }

      const renderTreeAsVTree =
      /** @type {VTree} */
      renderTree; // Always compare a fragment to a fragment. If the renderTree was not
      // wrapped, ensure it is here.

      if (renderTreeAsVTree.nodeType !== 11) {
        const isList = ('length' in renderTree);
        const renderTreeAsList =
        /** @type {VTree[]} */
        renderTree;
        renderTree = createTree(isList ? renderTreeAsList : [renderTreeAsVTree]);
      } // Put all the nodes together into a fragment for diffing.


      const fragment = createTree(childTrees);
      const tasks = [...Internals.defaultTasks];
      const syncTreesIndex = tasks.indexOf(Internals.tasks.syncTrees); // Inject a custom task after syncing has finished, but before patching has
      // occured. This gives us time to add additional patch logic per render.

      tasks.splice(syncTreesIndex + 1, 0, (
      /** @type {Transaction} */
      transaction) => {
        let lastTree = null; // Reconcile all top-level replacements and additions.

        for (let i = 0; i < fragment.childNodes.length; i++) {
          const childTree = fragment.childNodes[i]; // Replace if the nodes are different.

          if (childTree && childTrees[i] && childTrees[i] !== childTree) {
            transaction.patches.push(Internals.PATCH_TYPE.REPLACE_CHILD, childTree, childTrees[i]);
            lastTree = childTree;
          } // Add if there is no old Node.
          else if (lastTree && !childTrees[i]) {
            transaction.patches.push(Internals.PATCH_TYPE.INSERT_BEFORE, $$insertAfter, childTree, lastTree);
            lastTree = childTree;
          } // Keep the old node.
          else {
            lastTree = childTrees[i];
          }

          ComponentTreeCache.set(childTree, vTree);
        }

        return transaction;
      });
      /**
       * Compare the existing component node(s) to the new node(s).
       *
       * @type {Promise<Transaction>}
       */

      const promise = outerHTML(fragment, renderTree, {
        tasks
      });
      return promise.then(transaction => {
        // Empty the fragment after using.
        fragment.childNodes.length = 0;
        release(fragment);
        this.componentDidUpdate(this.props, this.state);
        return transaction;
      });
    }
  }, {
    key: "connectedCallback",
    value: function connectedCallback() {
      const {
        defaultProps = {}
      } =
      /** @type {any} */
      this.constructor; // This callback gets called during replace operations, there is no point
      // in re-rendering or creating a new shadow root due to this.
      // Always do a full render when mounting, so that something is visible.

      this[$$render]();
      this.componentDidMount();
      keys(defaultProps).forEach(propName => {
        defineProperty(this, propName, {
          get: () => this.props[propName],
          set: value => {
            this.props[propName] = value;
            this.componentWillReceiveProps(this.props, this.state);

            if (this.shouldComponentUpdate(this.props, this.state)) {
              this[$$render]();
            }
          }
        });
      });
    }
  }, {
    key: "disconnectedCallback",
    value: function disconnectedCallback() {
      this.componentWillUnmount();
    }
  }, {
    key: "attributeChangedCallback",
    value: function attributeChangedCallback() {
      const nextProps = createProps(this, this.props);
      const nextState = createState(this, this.state);
      this.componentWillReceiveProps(nextProps, nextState);

      if (this.shouldComponentUpdate(nextProps, nextState)) {
        this[$$render]();
      }
    }
    /**
     * @param {Props} props
     * @param {State} state
     *
     * @returns {boolean}
     */
    // @ts-ignore

  }, {
    key: "shouldComponentUpdate",
    value: function shouldComponentUpdate(props, state) {
      return true;
    }
  }, {
    key: "componentWillMount",
    value: function componentWillMount() {}
  }, {
    key: "componentDidMount",
    value: function componentDidMount() {}
    /**
     * @param {Props} props
     * @param {State} state
     */
    // @ts-ignore

  }, {
    key: "componentWillReceiveProps",
    value: function componentWillReceiveProps(props, state) {}
    /**
     * @param {Props} props
     * @param {State} state
     *
     * @returns {void}
     */
    // @ts-ignore

  }, {
    key: "componentDidUpdate",
    value: function componentDidUpdate(props, state) {}
  }, {
    key: "componentWillUnmount",
    value: function componentWillUnmount() {}
  }], [{
    key: "observedAttributes",
    get: function () {
      return getObserved(this).map(key => key.toLowerCase());
    }
    /**
     * Allow tests to unbind this task, you would not typically need to do this in
     * a web application, as this code loads once and is not reloaded.
     *
     * @type {Function | null}
     */

  }, {
    key: "subscribeMiddleware",
    value:
    /**
     * Adds the Component middleware, but first unregisters any previous
     * Component middleware.
     *
     * @returns {void}
     */
    function subscribeMiddleware() {
      const unsubscribe = Component[$$unsubscribe];
      unsubscribe && unsubscribe();
      Component[$$unsubscribe] = diff.use(middleware());
    }
    /**
     * Removes the Component middleware and clears the internal caches.
     *
     * @returns {void}
     */

  }, {
    key: "unsubscribeMiddleware",
    value: function unsubscribeMiddleware() {
      const unsubscribe = Component[$$unsubscribe];
      unsubscribe && unsubscribe();
      ComponentTreeCache.clear();
      InstanceCache.clear();
    }
  }]);

  return Component;
}(); // TODO: Can this be done inside the constructor so that class components do not
// need to inherit from HTMLElement?


Component[$$unsubscribe] = null;
export { Component as default };

try {
  setPrototypeOf(Component.prototype, HTMLElement.prototype);
} catch {} // Automatically subscribe the Component middleware.


Component.subscribeMiddleware();