'use strict';

Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });

const internals = require('./internals.cjs');
const constants = require('../constants.cjs');

/**
 * Builds on the browser’s [`MutationObserver`](https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver)
 * to detect changes in a remote element, and to communicate those changes in a way
 * that Remote DOM can understand. You create this object from a “remote
 * connection”, which you’ll generally get from the [`@remote-dom/core/receiver`](/packages/core#remote-domcorereceiver)
 * package. Then, you’ll observe changes in the HTML element that contains your
 * tree of remote elements.
 *
 * @example
 * import {RemoteMutationObserver} from '@remote-dom/core/elements';
 *
 * const observer = new RemoteMutationObserver(connection);
 *
 * // Now, any changes to the `body` element will be communicated
 * // to the host environment.
 * observer.observe(document.body);
 */
class RemoteMutationObserver extends MutationObserver {
  #observed;
  constructor(connection) {
    super(records => {
      const addedNodes = [];
      const remoteRecords = [];
      for (const record of records) {
        const targetId = internals.remoteId(record.target);
        if (record.type === 'childList') {
          const position = record.previousSibling ? indexOf(record.previousSibling, record.target.childNodes) + 1 : 0;
          record.removedNodes.forEach(node => {
            internals.disconnectRemoteNode(node);
            remoteRecords.push([constants.MUTATION_TYPE_REMOVE_CHILD, targetId, position]);
          });

          // A mutation observer will queue some changes, so we might get one record
          // for attaching a parent element, and additional records for attaching descendants.
          // We serialize the entire tree when a new node was added, so we don’t want to
          // send additional “insert child” records when we see those descendants — they
          // will already be included the insertion of the parent.
          record.addedNodes.forEach((node, index) => {
            if (addedNodes.some(addedNode => {
              return addedNode === node || addedNode.contains(node);
            })) {
              return;
            }
            addedNodes.push(node);
            internals.connectRemoteNode(node, connection);
            remoteRecords.push([constants.MUTATION_TYPE_INSERT_CHILD, targetId, internals.serializeRemoteNode(node), position + index]);
          });
        } else if (record.type === 'characterData') {
          remoteRecords.push([constants.MUTATION_TYPE_UPDATE_TEXT, targetId, record.target.textContent ?? '']);
        } else if (record.type === 'attributes' && record.attributeName != null && record.target instanceof Element && !record.target.tagName.includes('-')) {
          remoteRecords.push([constants.MUTATION_TYPE_UPDATE_PROPERTY, targetId, record.attributeName, record.target.getAttribute(record.attributeName)]);
        }
      }
      connection.mutate(remoteRecords);
    });
    this.connection = connection;
    this.#observed = new Set();
  }

  /**
   * Starts watching changes to the element, and communicates changes to the
   * host environment. By default, this method will also communicate any initial
   * children of the element to the host environment.
   */
  observe(target, options) {
    const id = options?.id ?? constants.ROOT_ID;
    internals.setRemoteId(target, id);
    this.#observed.add(target);
    if (options?.initial !== false && target.childNodes.length > 0) {
      if (id !== constants.ROOT_ID) {
        this.connection.mutate([[constants.MUTATION_TYPE_INSERT_CHILD, constants.ROOT_ID, internals.serializeRemoteNode(target), this.#observed.size - 1]]);
      } else if (target.childNodes.length > 0) {
        const records = [];
        for (let i = 0; i < target.childNodes.length; i++) {
          const node = target.childNodes[i];
          internals.connectRemoteNode(node, this.connection);
          records.push([constants.MUTATION_TYPE_INSERT_CHILD, constants.ROOT_ID, internals.serializeRemoteNode(node), i]);
        }
        this.connection.mutate(records);
      }
    }
    super.observe(target, {
      subtree: true,
      childList: true,
      attributes: true,
      characterData: true,
      ...options
    });
  }
  disconnect({
    empty = false
  } = {}) {
    if (empty && this.#observed.size > 0) {
      const records = [];
      for (const node of this.#observed) {
        internals.disconnectRemoteNode(node);
        const id = internals.remoteId(node);
        if (id === constants.ROOT_ID) {
          for (let i = 0; i < node.childNodes.length; i++) {
            records.push([constants.MUTATION_TYPE_REMOVE_CHILD, id, 0]);
          }
        } else {
          records.push([constants.MUTATION_TYPE_REMOVE_CHILD, constants.ROOT_ID, 0]);
        }
      }
      this.connection.mutate(records);
    }
    this.#observed.clear();
    super.disconnect();
  }
}
function indexOf(node, list) {
  for (let i = 0; i < list.length; i++) {
    if (list[i] === node) return i;
  }
  return -1;
}

exports.RemoteMutationObserver = RemoteMutationObserver;
