'use strict';

Object.defineProperty(exports, '__esModule', { value: true });

var dom = require('./dom-7e625b09.cjs');
var diff = require('./diff-9d236524.cjs');
var object = require('./object-c0c9435b.cjs');
var json = require('./json-092190a1.cjs');
var string = require('./string-fddc5f8b.cjs');
var array = require('./array-78849c95.cjs');
var number = require('./number-1fb57bba.cjs');
var _function = require('./function-314580f7.cjs');
require('./pair-ab022bc3.cjs');
require('./map-24d263c0.cjs');
require('./schema.cjs');
require('./error-0c1f634f.cjs');
require('./environment-1c97264d.cjs');
require('./conditions-f5c0c102.cjs');
require('./storage.cjs');
require('./equality.cjs');
require('./prng-37d48618.cjs');
require('./binary-ac8e39e2.cjs');
require('./math-96d5e8c4.cjs');
require('./buffer-3e750729.cjs');
require('./encoding-1a745c43.cjs');
require('./decoding-76e75827.cjs');
require('./set-5b47859e.cjs');

/* eslint-env browser */

/**
 * @type {CustomElementRegistry}
 */
const registry = customElements;

/**
 * @param {string} name
 * @param {any} constr
 * @param {ElementDefinitionOptions} [opts]
 */
const define = (name, constr, opts) => registry.define(name, constr, opts);

/**
 * @param {string} name
 * @return {Promise<CustomElementConstructor>}
 */
const whenDefined = name => registry.whenDefined(name);

const upgradedEventName = 'upgraded';
const connectedEventName = 'connected';
const disconnectedEventName = 'disconnected';

/**
 * @template S
 */
class Lib0Component extends HTMLElement {
  /**
   * @param {S} [state]
   */
  constructor (state) {
    super();
    /**
     * @type {S|null}
     */
    this.state = /** @type {any} */ (state);
    /**
     * @type {any}
     */
    this._internal = {};
  }

  /**
   * @param {S} _state
   * @param {boolean} [_forceStateUpdate] Force that the state is rerendered even if state didn't change
   */
  setState (_state, _forceStateUpdate = true) {}

  /**
    * @param {any} _stateUpdate
    */
  updateState (_stateUpdate) { }
}

/**
 * @param {any} val
 * @param {"json"|"string"|"number"} type
 * @return {string}
 */
const encodeAttrVal = (val, type) => {
  if (type === 'json') {
    val = json.stringify(val);
  }
  return val + ''
};

/**
 * @param {any} val
 * @param {"json"|"string"|"number"|"bool"} type
 * @return {any}
 */
const parseAttrVal = (val, type) => {
  switch (type) {
    case 'json':
      return json.parse(val)
    case 'number':
      return Number.parseFloat(val)
    case 'string':
      return val
    case 'bool':
      return val != null
    default:
      return null
  }
};

/**
 * @template S
 * @typedef {Object} CONF
 * @property {string?} [CONF.template] Template for the shadow dom.
 * @property {string} [CONF.style] shadow dom style. Is only used when
 * `CONF.template` is defined
 * @property {S} [CONF.state] Initial component state.
 * @property {function(S,S|null,Lib0Component<S>):void} [CONF.onStateChange] Called when
 * the state changes.
 * @property {Object<string,function(any, any):Object>} [CONF.childStates] maps from
 * CSS-selector to transformer function. The first element that matches the
 * CSS-selector receives state updates via the transformer function.
 * @property {Object<string,"json"|"number"|"string"|"bool">} [CONF.attrs]
 * attrs-keys and state-keys should be camelCase, but the DOM uses kebap-case. I.e.
 * `attrs = { myAttr: 4 }` is represeted as `<my-elem my-attr="4" />` in the DOM
 * @property {Object<string, function(CustomEvent, Lib0Component<any>):boolean|void>} [CONF.listeners] Maps from dom-event-name
 * to event listener.
 * @property {function(S, S, Lib0Component<S>):Object<string,string>} [CONF.slots] Fill slots
 * automatically when state changes. Maps from slot-name to slot-html.
 */

/**
 * @template T
 * @param {string} name
 * @param {CONF<T>} cnf
 * @return {typeof Lib0Component}
 */
const createComponent = (name, { template, style = '', state: defaultState, onStateChange = () => {}, childStates = { }, attrs = {}, listeners = {}, slots = () => ({}) }) => {
  /**
   * Maps from camelCase attribute name to kebap-case attribute name.
   * @type {Object<string,string>}
   */
  const normalizedAttrs = {};
  for (const key in attrs) {
    normalizedAttrs[string.fromCamelCase(key, '-')] = key;
  }
  const templateElement = template
    ? /** @type {HTMLTemplateElement} */ (dom.parseElement(`
      <template>
        <style>${style}</style>
        ${template}
      </template>
      `))
    : null;

  class _Lib0Component extends HTMLElement {
    /**
     * @param {T} [state]
     */
    constructor (state) {
      super();
      /**
       * @type {Array<{d:Lib0Component<T>, s:function(any, any):Object}>}
       */
      this._childStates = [];
      /**
       * @type {Object<string,string>}
       */
      this._slots = {};
      this._init = false;
      /**
       * @type {any}
       */
      this._internal = {};
      /**
       * @type {any}
       */
      this.state = state || null;
      this.connected = false;
      // init shadow dom
      if (templateElement) {
        const shadow = /** @type {ShadowRoot} */ (this.attachShadow({ mode: 'open' }));
        shadow.appendChild(templateElement.content.cloneNode(true));
        // fill child states
        for (const key in childStates) {
          this._childStates.push({
            d: /** @type {Lib0Component<T>} */ (dom.querySelector(/** @type {any} */ (shadow), key)),
            s: childStates[key]
          });
        }
      }
      dom.emitCustomEvent(this, upgradedEventName, { bubbles: true });
    }

    connectedCallback () {
      this.connected = true;
      if (!this._init) {
        this._init = true;
        const shadow = this.shadowRoot;
        if (shadow) {
          dom.addEventListener(shadow, upgradedEventName, event => {
            this.setState(this.state, true);
            event.stopPropagation();
          });
        }
        /**
         * @type {Object<string, any>}
         */
        const startState = this.state || object.assign({}, defaultState);
        if (attrs) {
          for (const key in attrs) {
            const normalizedKey = string.fromCamelCase(key, '-');
            const val = parseAttrVal(this.getAttribute(normalizedKey), attrs[key]);
            if (val) {
              startState[key] = val;
            }
          }
        }
        // add event listeners
        for (const key in listeners) {
          dom.addEventListener(shadow || this, key, event => {
            if (listeners[key](/** @type {CustomEvent} */ (event), this) !== false) {
              event.stopPropagation();
              event.preventDefault();
              return false
            }
          });
        }
        // first setState call
        this.state = null;
        this.setState(startState);
      }
      dom.emitCustomEvent(/** @type {any} */ (this.shadowRoot || this), connectedEventName, { bubbles: true });
    }

    disconnectedCallback () {
      this.connected = false;
      dom.emitCustomEvent(/** @type {any} */ (this.shadowRoot || this), disconnectedEventName, { bubbles: true });
      this.setState(null);
    }

    static get observedAttributes () {
      return object.keys(normalizedAttrs)
    }

    /**
     * @param {string} name
     * @param {string} oldVal
     * @param {string} newVal
     *
     * @private
     */
    attributeChangedCallback (name, oldVal, newVal) {
      const curState = /** @type {any} */ (this.state);
      const camelAttrName = normalizedAttrs[name];
      const type = attrs[camelAttrName];
      const parsedVal = parseAttrVal(newVal, type);
      if (curState && (type !== 'json' || json.stringify(curState[camelAttrName]) !== newVal) && curState[camelAttrName] !== parsedVal && !number.isNaN(parsedVal)) {
        this.updateState({ [camelAttrName]: parsedVal });
      }
    }

    /**
     * @param {any} stateUpdate
     */
    updateState (stateUpdate) {
      this.setState(object.assign({}, this.state, stateUpdate));
    }

    /**
     * @param {any} state
     */
    setState (state, forceStateUpdates = false) {
      const prevState = this.state;
      this.state = state;
      if (this._init && (!_function.equalityFlat(state, prevState) || forceStateUpdates)) {
        // fill slots
        if (state) {
          const slotElems = slots(state, prevState, this);
          for (const key in slotElems) {
            const slotContent = slotElems[key];
            if (this._slots[key] !== slotContent) {
              this._slots[key] = slotContent;
              const currentSlots = /** @type {Array<any>} */ (key !== 'default' ? array.from(dom.querySelectorAll(this, `[slot="${key}"]`)) : array.from(this.childNodes).filter(/** @param {any} child */ child => !dom.checkNodeType(child, dom.ELEMENT_NODE) || !child.hasAttribute('slot')));
              currentSlots.slice(1).map(dom.remove);
              const nextSlot = dom.parseFragment(slotContent);
              if (key !== 'default') {
                array.from(nextSlot.children).forEach(c => c.setAttribute('slot', key));
              }
              if (currentSlots.length > 0) {
                dom.replaceWith(currentSlots[0], nextSlot);
              } else {
                dom.appendChild(this, nextSlot);
              }
            }
          }
        }
        onStateChange(state, prevState, this);
        if (state != null) {
          this._childStates.forEach(cnf => {
            const d = cnf.d;
            if (d.updateState) {
              d.updateState(cnf.s(state, this));
            }
          });
        }
        for (const key in attrs) {
          const normalizedKey = string.fromCamelCase(key, '-');
          if (state == null) {
            this.removeAttribute(normalizedKey);
          } else {
            const stateVal = state[key];
            const attrsType = attrs[key];
            if (!prevState || prevState[key] !== stateVal) {
              if (attrsType === 'bool') {
                if (stateVal) {
                  this.setAttribute(normalizedKey, '');
                } else {
                  this.removeAttribute(normalizedKey);
                }
              } else if (stateVal == null && (attrsType === 'string' || attrsType === 'number')) {
                this.removeAttribute(normalizedKey);
              } else {
                this.setAttribute(normalizedKey, encodeAttrVal(stateVal, attrsType));
              }
            }
          }
        }
      }
    }
  }
  define(name, _Lib0Component);
  // @ts-ignore
  return _Lib0Component
};

/**
 * @param {function} definer function that defines a component when executed
 */
const createComponentDefiner = definer => {
  /**
   * @type {any}
   */
  let defined = null;
  return () => {
    if (!defined) {
      defined = definer();
    }
    return defined
  }
};

const defineListComponent = createComponentDefiner(() => {
  const ListItem = createComponent('lib0-list-item', {
    template: '<slot name="content"></slot>',
    slots: state => ({
      content: `<div>${state}</div>`
    })
  });
  return createComponent('lib0-list', {
    state: { list: /** @type {Array<string>} */ ([]), Item: ListItem },
    onStateChange: (state, prevState, component) => {
      if (state == null) {
        return
      }
      const { list = /** @type {Array<any>} */ ([]), Item = ListItem } = state;
      // @todo deep compare here by providing another parameter to simpleDiffArray
      let { index, remove, insert } = diff.simpleDiffArray(prevState ? prevState.list : [], list, _function.equalityFlat);
      if (remove === 0 && insert.length === 0) {
        return
      }
      let child = /** @type {Lib0Component<any>} */ (component.firstChild);
      while (index-- > 0) {
        child = /** @type {Lib0Component<any>} */ (child.nextElementSibling);
      }
      let insertStart = 0;
      while (insertStart < insert.length && remove-- > 0) {
        // update existing state
        child.setState(insert[insertStart++]);
        child = /** @type {Lib0Component<any>} */ (child.nextElementSibling);
      }
      while (remove-- > 0) {
        // remove remaining
        const prevChild = child;
        child = /** @type {Lib0Component<any>} */ (child.nextElementSibling);
        component.removeChild(prevChild);
      }
      // insert remaining
      component.insertBefore(dom.fragment(insert.slice(insertStart).map(/** @param {any} insState */ insState => {
        const el = new Item();
        el.setState(insState);
        return el
      })), child);
    }
  })
});

const defineLazyLoadingComponent = createComponentDefiner(() => createComponent('lib0-lazy', {
  state: /** @type {{component:null|String,import:null|function():Promise<any>,state:null|object}} */ ({
    component: null, import: null, state: null
  }),
  attrs: {
    component: 'string'
  },
  onStateChange: ({ component, state, import: getImport }, prevState, componentEl) => {
    if (component !== null) {
      if (getImport) {
        getImport();
      }
      if (!prevState || component !== prevState.component) {
        const el = /** @type {any} */ (dom.createElement(component));
        componentEl.innerHTML = '';
        componentEl.insertBefore(el, null);
      }
      const el = /** @type {any} */ (componentEl.firstElementChild);
      // @todo generalize setting state and check if setState is defined
      if (el.setState) {
        el.setState(state);
      }
    }
  }
}));

exports.Lib0Component = Lib0Component;
exports.createComponent = createComponent;
exports.createComponentDefiner = createComponentDefiner;
exports.define = define;
exports.defineLazyLoadingComponent = defineLazyLoadingComponent;
exports.defineListComponent = defineListComponent;
exports.registry = registry;
exports.whenDefined = whenDefined;
//# sourceMappingURL=component.cjs.map
