import { kebabCaseJoin } from '../helpers';
import { forwardAttrs } from '../helpers/componentHelpers';
import { createStyleMixin } from './createStyleMixin';
import { createCssVarsList } from './createStyleMixin/helpers';

// this is needed because we might generate the same css vars names
// e.g. "overlayColor" attribute in style mixin's mapping,
// will generate the same var as "color" attribute in portals's mapping
// when the portal name is "overlay".
// because of that, we are adding this separator that is also used as a differentiator
const DISPLAY_NAME_SEPARATOR = '_';

const sanitizeSelector = (selector) => selector.replace(/[^\w\s]/gi, '');

const withWaitForShadowRoot = (getRootElementFn) => async (that) => {
  const ele = await getRootElementFn(that);

  return new Promise((res) => {
    const MAX_RETRIES = 20;
    let counter = 0;

    const check = () => {
      if (counter > MAX_RETRIES) {
        // eslint-disable-next-line no-console
        console.error('could not get shadow root for element', ele);
        res(ele);
        return;
      }

      counter++;

      if (!ele?.shadowRoot) {
        setTimeout(check);
      } else {
        res(ele.shadowRoot);
      }
    };
    check();
  });
};

export const portalMixin =
  ({ name, selector, mappings = {}, forward: { attributes = [], include = true } = {} }) =>
  (superclass) => {
    const eleDisplayName = name || sanitizeSelector(selector);

    const BaseClass = createStyleMixin({
      mappings,
    })(superclass);

    return class PortalMixinClass extends BaseClass {
      static get cssVarList() {
        return {
          ...BaseClass.cssVarList,
          [eleDisplayName]: createCssVarsList(
            eleDisplayName
              ? kebabCaseJoin(superclass.componentName, DISPLAY_NAME_SEPARATOR + eleDisplayName)
              : superclass.componentName,
            mappings
          ),
        };
      }

      #portalEle;

      constructor() {
        // we cannot use "this" before calling "super"
        const getRootElement = async (that) => {
          const baseEle = (that.shadowRoot || that).querySelector(that.baseSelector);
          if (!selector || !that.shadowRoot) {
            return baseEle;
          }

          // in case we have a selector, we should first wait for the base element shadow root
          //  and then look for the internal element
          const baseEleShadowRoot = await withWaitForShadowRoot(() => baseEle)(that);
          return baseEleShadowRoot.querySelector(selector);
        };

        const getPortalElement = withWaitForShadowRoot(getRootElement);

        super({
          getRootElement: getPortalElement,
          componentNameSuffix: eleDisplayName && DISPLAY_NAME_SEPARATOR + eleDisplayName,
          themeSection: eleDisplayName ? DISPLAY_NAME_SEPARATOR + eleDisplayName : undefined,
          baseSelector: '',
        });

        // Defer portal element lookup to init() for React compatibility
        // (elements created in init() won't exist during constructor)
        this.#getPortalElement = () => getPortalElement(this).then((ele) => ele?.host || ele);
      }

      #getPortalElement;

      async #handleHoverAttribute() {
        const portalEle = await this.#portalEle;
        portalEle.onmouseenter = (e) => {
          e.target.setAttribute('hover', 'true');
        };
        portalEle.onmouseleave = (e) => {
          e.target.removeAttribute('hover');
        };
      }

      async init() {
        super.init?.();
        // Initialize portal element lookup here (deferred from constructor for React compatibility)
        this.#portalEle = this.#getPortalElement();
        const portalEle = await this.#portalEle;
        forwardAttrs(this, portalEle, {
          [include ? 'includeAttrs' : 'excludeAttrs']: attributes,
        });

        this.#handleHoverAttribute();
      }
    };
  };
