/**
 * @internal
 */
const CONTROLLER = '__FUIDT_CONTROLLER__';

/**
 * @internal
 */
const ELEMENT_METADATA = '__FUIDT_ELEMENT_METADATA__';

/**
 * @internal
 */
const HTML_ELEMENT_REFERENCE = '__FUIDT_HTML_ELEMENT_REFERENCE__';

/**
 * @internal
 */
const SERIALIZED_DATA_CHANGE = '__FUIDT_SERIALIZED_DATA_CHANGE__';

/**
 * Verifies if a given node is an HTMLElement,
 * this method works seamlessly with frames and elements from different documents
 *
 * This is preferred over simply using `instanceof`.
 * Since `instanceof` might be problematic while operating with [multiple realms](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/instanceof#instanceof_and_multiple_realms)
 *
 * @example
 * ```ts
 * isHTMLElement(event.target) && event.target.focus()
 * isHTMLElement(event.target, {constructorName: 'HTMLInputElement'}) && event.target.value // some value
 * ```
 *
 */
function isHTMLElement(element, options) {
  var _typedElement$ownerDo, _options$constructorN;
  const typedElement = element;
  return Boolean((typedElement == null || (_typedElement$ownerDo = typedElement.ownerDocument) == null ? void 0 : _typedElement$ownerDo.defaultView) && typedElement instanceof typedElement.ownerDocument.defaultView[(_options$constructorN = void 0 ) != null ? _options$constructorN : 'HTMLElement']);
}

/**
 * @internal
 */

const isHTMLElementWithMetadata = element => Boolean(isHTMLElement(element) && ELEMENT_METADATA in element && element.parentElement !== null);

const createController = defaultView => {
  let selectedElement = null;
  const observer = new MutationObserver(mutations => {
    if (!selectedElement) {
      return;
    }
    for (const mutation of mutations) {
      if (mutation.type === 'childList' && Array.from(mutation.removedNodes).includes(selectedElement)) {
        controller.withdraw();
      }
    }
  });
  const controller = {
    get selectedElement() {
      return selectedElement;
    },
    select: nextSelectedElement => {
      if (isHTMLElementWithMetadata(nextSelectedElement)) {
        selectedElement = nextSelectedElement;
        observer.observe(nextSelectedElement.parentElement, {
          childList: true,
          subtree: false
        });
      }
      if (selectedElement && nextSelectedElement) {
        const metadata = selectedElement[ELEMENT_METADATA];
        if (metadata.references.has(nextSelectedElement)) {
          return selectedElement;
        }
      }
      controller.withdraw();
      return selectedElement;
    },
    withdraw: () => {
      selectedElement = null;
      observer.disconnect();
      defaultView.postMessage(SERIALIZED_DATA_CHANGE);
    }
  };
  return controller;
};
const injectController = _ref => {
  let {
    defaultView
  } = _ref;
  if (!defaultView) {
    return;
  }
  if (!defaultView[CONTROLLER]) {
    defaultView[CONTROLLER] = createController(defaultView);
  }
};
const getController = targetDocument => {
  var _targetDocument$defau, _targetDocument$defau2;
  injectController(targetDocument);
  return (_targetDocument$defau = (_targetDocument$defau2 = targetDocument.defaultView) == null ? void 0 : _targetDocument$defau2[CONTROLLER]) != null ? _targetDocument$defau : null;
};

const serialize = (data, references) => {
  const serializedData = JSON.parse(JSON.stringify(data, (_, value) => {
    if (isHTMLElement(value)) return references.add(value);
    if (typeof value === 'object' && value && Object.getPrototypeOf(value) !== Object.prototype && Object.getPrototypeOf(value) !== Array.prototype) {
      if ('toString' in value) {
        return value.toString();
      }
      return undefined;
    }
    return value;
  }));
  return serializedData;
};

let counter = 0;
const generateReferenceId = () => HTML_ELEMENT_REFERENCE + ":" + counter++;
const createReferences = () => {
  const map = new Map();
  const weakMap = new WeakMap();
  const references = {
    add: element => {
      if (weakMap.has(element)) {
        return weakMap.get(element);
      }
      const id = generateReferenceId();
      map.set(id, element);
      weakMap.set(element, id);
      return id;
    },
    get: id => {
      const element = map.get(id);
      if (element && weakMap.has(element)) {
        return element;
      }
    },
    has: element => {
      return weakMap.has(element);
    }
  };
  return references;
};

/**
 * devtools middleware
 * @public
 */
const devtools = function (targetDocument, middlewareDataCallback) {
  if (targetDocument === void 0) {
    targetDocument = document;
  }
  if (middlewareDataCallback === void 0) {
    middlewareDataCallback = floatingUIMiddlewareDataCallback;
  }
  return {
    name: '@floating-ui/devtools',
    fn: state => {
      const {
        [ELEMENT_METADATA]: metadata
      } = isHTMLElementWithMetadata(state.elements.floating) ? state.elements.floating : Object.assign(state.elements.floating, {
        [ELEMENT_METADATA]: {
          references: createReferences(),
          serializedData: []
        }
      });
      const serializedData = serialize(middlewareDataCallback(state), metadata.references);
      metadata.serializedData.unshift(serializedData);
      const controller = getController(targetDocument);
      if (metadata.serializedData.length > 1 && state.elements.floating === (controller == null ? void 0 : controller.selectedElement)) {
        var _targetDocument$defau;
        (_targetDocument$defau = targetDocument.defaultView) == null || _targetDocument$defau.postMessage(SERIALIZED_DATA_CHANGE);
      }
      return {};
    }
  };
};
const floatingUIMiddlewareDataCallback = state => ({
  ...state,
  type: 'FloatingUIMiddleware'
});

export { devtools, devtools as middleware };
