/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

import type {StyleXPlugin} from 'react-devtools-shared/src/frontend/types';
import isArray from 'react-devtools-shared/src/isArray';

const cachedStyleNameToValueMap: Map<string, string> = new Map();

export function getStyleXData(data: any): StyleXPlugin {
  const sources = new Set<string>();
  const resolvedStyles = {};

  crawlData(data, sources, resolvedStyles);

  return {
    sources: Array.from(sources).sort(),
    resolvedStyles,
  };
}

export function crawlData(
  data: any,
  sources: Set<string>,
  resolvedStyles: Object,
): void {
  if (data == null) {
    return;
  }

  if (isArray(data)) {
    data.forEach(entry => {
      if (entry == null) {
        return;
      }

      if (isArray(entry)) {
        crawlData(entry, sources, resolvedStyles);
      } else {
        crawlObjectProperties(entry, sources, resolvedStyles);
      }
    });
  } else {
    crawlObjectProperties(data, sources, resolvedStyles);
  }

  resolvedStyles = Object.fromEntries<string, any>(
    Object.entries(resolvedStyles).sort(),
  );
}

function crawlObjectProperties(
  entry: Object,
  sources: Set<string>,
  resolvedStyles: Object,
): void {
  const keys = Object.keys(entry);
  keys.forEach(key => {
    const value = entry[key];
    if (typeof value === 'string') {
      if (key === value) {
        // Special case; this key is the name of the style's source/file/module.
        sources.add(key);
      } else {
        const propertyValue = getPropertyValueForStyleName(value);
        if (propertyValue != null) {
          resolvedStyles[key] = propertyValue;
        }
      }
    } else {
      const nestedStyle = {};
      resolvedStyles[key] = nestedStyle;
      crawlData([value], sources, nestedStyle);
    }
  });
}

function getPropertyValueForStyleName(styleName: string): string | null {
  if (cachedStyleNameToValueMap.has(styleName)) {
    return ((cachedStyleNameToValueMap.get(styleName): any): string);
  }

  for (
    let styleSheetIndex = 0;
    styleSheetIndex < document.styleSheets.length;
    styleSheetIndex++
  ) {
    const styleSheet = ((document.styleSheets[
      styleSheetIndex
    ]: any): CSSStyleSheet);
    let rules: CSSRuleList | null = null;
    // this might throw if CORS rules are enforced https://www.w3.org/TR/cssom-1/#the-cssstylesheet-interface
    try {
      rules = styleSheet.cssRules;
    } catch (_e) {
      continue;
    }

    for (let ruleIndex = 0; ruleIndex < rules.length; ruleIndex++) {
      if (!(rules[ruleIndex] instanceof CSSStyleRule)) {
        continue;
      }
      const rule = ((rules[ruleIndex]: any): CSSStyleRule);
      const {cssText, selectorText, style} = rule;

      if (selectorText != null) {
        if (selectorText.startsWith(`.${styleName}`)) {
          const match = cssText.match(/{ *([a-z\-]+):/);
          if (match !== null) {
            const property = match[1];
            const value = style.getPropertyValue(property);

            cachedStyleNameToValueMap.set(styleName, value);

            return value;
          } else {
            return null;
          }
        }
      }
    }
  }

  return null;
}
