class Component {
  constructor({ store, tag = 'div', data } = {}) {
    this.store = store; // reference to a store
    if (!store && typeof Store == 'function' && window.store instanceof Store)
      this.store = window.store;

    this.data = data || {}; // a place for local component data
    this.el = {}; // a place for local component elements

    this._root(tag); // make this.root element
    if (this.create) this.create(tag => { this._root(tag); });

    this.root.component = this; // ref so component is not garbage collected

    if (!this.store) return this.root;
    this.store.listen(this.root, 'component');
    this.store.subscribe(this, (type, data) => { this._on(type, data); });
    return this.root;
  }

  // emit event on root element
  emit(type, data) {
    const ev = new CustomEvent(type, { detail: data });
    const wrappedEv = new CustomEvent('component', { detail: { type, data } });
    this.root.dispatchEvent(ev);
    this.root.dispatchEvent(wrappedEv);
    this._on(type, data);
  }

  // clear references to enable garbage collection on the component
  destroy() {
    if (this.onDestroy) this.onDestroy();
    if (this.store) this.store.disconnect(this.root, this);
    if (this.root.parentNode) this.root.parentNode.removeChild(this.root);
    if (this._mob) this._mob.disconnect();
  }

  // construct root element
  _root(tag) {
    if (this.root && this._mob) this._mob.disconnect();

    this.root = document.createElement(tag);
    this.root.setAttribute('component', this.constructor.name.toLowerCase());

    // watch DOM for element mount and unmount
    const inspects = [];
    inspects.push({ att: 'addedNodes', act: () => this.emit('mount') });
    inspects.push({ att: 'removedNodes', act: () => this.emit('unmount') });
    let current = 0;
    const mutation = records => {
      for (let i = 0; i < records.length; i++)
        for (let j = 0; j < records[i][inspects[current].att].length; j++)
          if (records[i][inspects[current].att][j] == this.root) {
            inspects[current].act();
            current = (current + 1) % inspects.length;
          }
    };

    const mobInit = { childList: true, subtree: true };
    this._mob = new MutationObserver(mutation);
    this._mob.observe(this.root.ownerDocument.body, mobInit);
  }

  // incoming events trigger onEvent methods if available
  _on(type, data) {
    const onEvent = `on-${type}`.replace(/-[^-]/g, s => s[1].toUpperCase());
    if (this[onEvent]) this[onEvent](data);
  }
}
