import { injectStyle } from '@descope-ui/common/components-helpers';
import { BASE_THEME_SECTION, CSS_SELECTOR_SPECIFIER_MULTIPLY } from '../../constants';
import { kebabCaseJoin } from '../../helpers';
import { getCssVarName, observeAttributes } from '../../helpers/componentHelpers';
import { componentsThemeManager } from '../../helpers/themeHelpers';
import { createStyle, createCssVarsList, createClassSelectorSpecifier } from './helpers';

const STYLE_OVERRIDE_ATTR_PREFIX = 'st';

export const createStyleMixin =
  ({ mappings = {}, componentNameOverride = '' }) =>
  (superclass) => {
    const componentName = componentNameOverride || superclass.componentName;

    return class CustomStyleMixinClass extends superclass {
      static get cssVarList() {
        return {
          ...superclass.cssVarList,
          ...createCssVarsList(componentName, {
            ...mappings,
          }),
        };
      }

      #overrideStyleEle;

      #themeStyleEle;

      #disconnectThemeManager;

      #componentNameSuffix;

      #themeSection;

      #rootElement;

      #baseSelector;

      #styleAttributes;

      #getRootElement;

      // we are using this mixin also for portalMixin
      // so we should be able to inject styles to other DOM elements
      // this is why we need to support these overrides
      constructor({
        getRootElement,
        componentNameSuffix = '',
        themeSection = BASE_THEME_SECTION,
        baseSelector,
      } = {}) {
        super();
        this.#componentNameSuffix = componentNameSuffix;
        this.#themeSection = themeSection;
        this.#baseSelector = baseSelector ?? this.baseSelector;
        this.#getRootElement = getRootElement;

        this.#styleAttributes = Object.keys(CustomStyleMixinClass.cssVarList).map((key) =>
          kebabCaseJoin(STYLE_OVERRIDE_ATTR_PREFIX, componentNameSuffix, key)
        );
      }

      get #componentTheme() {
        return componentsThemeManager.currentTheme?.[componentName] || '';
      }

      #onComponentThemeChange() {
        this.#themeStyleEle.replaceSync(this.#componentTheme[this.#themeSection]);
      }

      #createComponentTheme() {
        this.#themeStyleEle = injectStyle('', this.#rootElement, { prepend: true });
        this.#disconnectThemeManager = componentsThemeManager.onCurrentThemeChange(
          this.#onComponentThemeChange.bind(this)
        );
        this.#onComponentThemeChange();
      }

      #createOverridesStyle() {
        if (this.#styleAttributes.length) {
          let classSpecifier = createClassSelectorSpecifier(
            componentName,
            CSS_SELECTOR_SPECIFIER_MULTIPLY
          );
          const elementId = this.getAttribute('id');
          if (elementId) {
            // basically this is enough to make the selector more specific
            // but just in case there is no id, we will also add the class multiple times
            classSpecifier += `#${CSS.escape(elementId)}`;
          }

          this.#overrideStyleEle = injectStyle(`:host(${classSpecifier}) {}`, this.#rootElement);
        }
      }

      #setAttrOverride(attrName, value) {
        const { style } = this.#overrideStyleEle.cssRules[0];

        if (!style) return;

        const varName = getCssVarName(
          componentName,
          attrName.replace(new RegExp(`^${STYLE_OVERRIDE_ATTR_PREFIX}-`), '')
        );

        if (value) style?.setProperty(varName, value);
        else {
          style?.removeProperty(varName);
          this.removeAttribute(attrName);
        }
      }

      #updateOverridesStyle(attrs = []) {
        attrs.forEach((attr) => {
          if (this.#styleAttributes.includes(attr)) {
            this.#setAttrOverride(attr, this.getAttribute(attr));
          }
        });
      }

      #createMappingStyle() {
        if (Object.keys(mappings).length) {
          const style = createStyle(
            kebabCaseJoin(componentName, this.#componentNameSuffix),
            this.#baseSelector,
            mappings
          );

          injectStyle(style, this.#rootElement, { prepend: true });
        }
      }

      #addClassName(className) {
        (this.#rootElement.classList || this.#rootElement.host.classList).add(className);
      }

      async init() {
        super.init?.();
        if (this.shadowRoot?.isConnected) {
          this.#rootElement = (await this.#getRootElement?.(this)) || this.shadowRoot;

          this.#addClassName(componentName);

          this.#createMappingStyle();
          this.#createComponentTheme();
          this.#createOverridesStyle();

          // this is instead attributeChangedCallback because we cannot use static methods in this case
          observeAttributes(this, this.#updateOverridesStyle.bind(this), {});
        }
      }

      disconnectedCallback() {
        super.disconnectedCallback?.();

        this.#disconnectThemeManager?.();
      }
    };
  };
