// Copyright 2022 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import * as i18n from '../../core/i18n/i18n.js';
import * as SDK from '../../core/sdk/sdk.js';

import {
  buildPropertyDefinitionText,
  buildPropertyName,
  buildPropertyValue,
  isBlockContainer,
  isFlexContainer,
  isGridContainer,
  isGridLanesContainer,
  isInlineElement,
  isMulticolContainer,
  isPossiblyReplacedElement,
} from './CSSRuleValidatorHelper.js';

const UIStrings = {
  /**
   * @description The message shown in the Style pane when the user hovers over a property that has no effect due to some other property.
   * @example {flex-wrap: nowrap} REASON_PROPERTY_DECLARATION_CODE
   * @example {align-content} AFFECTED_PROPERTY_DECLARATION_CODE
   */
  ruleViolatedBySameElementRuleReason:
      'The {REASON_PROPERTY_DECLARATION_CODE} property prevents {AFFECTED_PROPERTY_DECLARATION_CODE} from having an effect.',
  /**
   * @description The message shown in the Style pane when the user hovers over a property declaration that has no effect due to some other property.
   * @example {flex-wrap} PROPERTY_NAME
   * @example {nowrap} PROPERTY_VALUE
   */
  ruleViolatedBySameElementRuleFix: 'Try setting {PROPERTY_NAME} to something other than {PROPERTY_VALUE}.',
  /**
   * @description The message shown in the Style pane when the user hovers over a property declaration that has no effect due to not being a flex or grid container.
   * @example {display: grid} DISPLAY_GRID_RULE
   * @example {display: flex} DISPLAY_FLEX_RULE
   */
  ruleViolatedBySameElementRuleChangeFlexOrGrid:
      'Try adding {DISPLAY_GRID_RULE} or {DISPLAY_FLEX_RULE} to make this element into a container.',
  /**
   * @description The message shown in the Style pane when the user hovers over a property declaration that has no effect due to the current property value.
   * @example {display: block} EXISTING_PROPERTY_DECLARATION
   * @example {display: flex} TARGET_PROPERTY_DECLARATION
   */
  ruleViolatedBySameElementRuleChangeSuggestion:
      'Try setting the {EXISTING_PROPERTY_DECLARATION} property to {TARGET_PROPERTY_DECLARATION}.',
  /**
   * @description The message shown in the Style pane when the user hovers over a property declaration that has no effect due to properties of the parent element.
   * @example {display: block} REASON_PROPERTY_DECLARATION_CODE
   * @example {flex} AFFECTED_PROPERTY_DECLARATION_CODE
   */
  ruleViolatedByParentElementRuleReason:
      'The {REASON_PROPERTY_DECLARATION_CODE} property on the parent element prevents {AFFECTED_PROPERTY_DECLARATION_CODE} from having an effect.',
  /**
   * @description The message shown in the Style pane when the user hovers over a property declaration that has no effect due to the properties of the parent element.
   * @example {display: block} EXISTING_PARENT_ELEMENT_RULE
   * @example {display: flex} TARGET_PARENT_ELEMENT_RULE
   */
  ruleViolatedByParentElementRuleFix:
      'Try setting the {EXISTING_PARENT_ELEMENT_RULE} property on the parent to {TARGET_PARENT_ELEMENT_RULE}.',

  /**
   * @description The warning text shown in Elements panel when font-variation-settings don't match allowed values
   * @example {wdth} PH1
   * @example {100} PH2
   * @example {10} PH3
   * @example {20} PH4
   * @example {Arial} PH5
   */
  fontVariationSettingsWarning:
      'Value for setting “{PH1}” {PH2} is outside the supported range [{PH3}, {PH4}] for font-family “{PH5}”.',
  /**
   * @description The message shown in the Style pane when the user hovers over a property declaration that has no effect on flex or grid child items.
   * @example {flex} CONTAINER_DISPLAY_NAME
   * @example {align-contents} PROPERTY_NAME
   */
  flexGridContainerPropertyRuleReason:
      'This element is a {CONTAINER_DISPLAY_NAME} item, i.e. a child of a {CONTAINER_DISPLAY_NAME} container, but {PROPERTY_NAME} only applies to containers.',
  /**
   * @description The message shown in the Style pane when the user hovers over a property declaration that has no effect on flex or grid child items.
   * @example {align-contents} PROPERTY_NAME
   * @example {align-self} ALTERNATIVE_PROPERTY_NAME
   */
  flexGridContainerPropertyRuleFix:
      'Try setting the {PROPERTY_NAME} on the container element or use {ALTERNATIVE_PROPERTY_NAME} instead.',
  /**
   * @description The messages shown in the Style pane when the user hovers over a position-anchor declaration that has no affect on a non-anchor-positioned element.
   * @example {relative} POSITION
   */
  invalidAnchorPositioning:
      'An anchor was defined but the element was not anchor-positioned but positioned "{POSITION}".',
  /**
   * @description The messages shown in the Style pane when the user hovers over a position-anchor declaration that has no affect on a non-anchor-positioned element.
   */
  invalidAnchorPositioningFix: 'Set position to either "fixed" or "absolute".',
  /**
   * @description The messages shown in the Style pane when the user hovers over a position-anchor declaration that has no affect on hidden element.
   */
  unusedAnchorPositioning: 'An anchor was defined but the element is hidden.',
} as const;
const str_ = i18n.i18n.registerUIStrings('panels/elements/CSSRuleValidator.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);

export const enum HintType {
  INACTIVE_PROPERTY = 'ruleValidation',
  DEPRECATED_PROPERTY = 'deprecatedProperty',
}

export class Hint {
  readonly #hintMessage: string;
  readonly #possibleFixMessage: string|null;
  readonly #learnMoreLink: string|undefined;

  constructor(hintMessage: string, possibleFixMessage: string|null, learnMoreLink?: string) {
    this.#hintMessage = hintMessage;
    this.#possibleFixMessage = possibleFixMessage;
    this.#learnMoreLink = learnMoreLink;
  }

  getMessage(): string {
    return this.#hintMessage;
  }

  getPossibleFixMessage(): string|null {
    return this.#possibleFixMessage;
  }

  getLearnMoreLink(): string|undefined {
    return this.#learnMoreLink;
  }
}

export abstract class CSSRuleValidator {
  readonly #affectedProperties: string[];

  constructor(affectedProperties: string[]) {
    this.#affectedProperties = affectedProperties;
  }

  getApplicableProperties(): string[] {
    return this.#affectedProperties;
  }

  abstract getHint(
      propertyName: string, computedStyles?: Map<string, string>, parentComputedStyles?: Map<string, string>,
      nodeName?: string, fontFaces?: SDK.CSSFontFace.CSSFontFace[]): Hint|undefined;
}

export class AlignContentValidator extends CSSRuleValidator {
  constructor() {
    super(['align-content', 'place-content']);
  }

  getHint(_propertyName: string, computedStyles?: Map<string, string>): Hint|undefined {
    if (!computedStyles) {
      return;
    }
    const isFlex = isFlexContainer(computedStyles);
    if (!isFlex && !isBlockContainer(computedStyles) && !isGridContainer(computedStyles) &&
        !isGridLanesContainer(computedStyles)) {
      const reasonPropertyDeclaration = buildPropertyDefinitionText('display', computedStyles?.get('display'));
      const affectedPropertyDeclarationCode = buildPropertyName('align-content');

      return new Hint(
          i18nString(UIStrings.ruleViolatedBySameElementRuleReason, {
            REASON_PROPERTY_DECLARATION_CODE: reasonPropertyDeclaration,
            AFFECTED_PROPERTY_DECLARATION_CODE: affectedPropertyDeclarationCode,
          }),
          i18nString(UIStrings.ruleViolatedBySameElementRuleFix, {
            PROPERTY_NAME: buildPropertyName('display'),
            PROPERTY_VALUE: buildPropertyValue(computedStyles?.get('display') as string),
          }),
      );
    }

    if (!isFlex) {
      return;
    }
    if (computedStyles.get('flex-wrap') !== 'nowrap') {
      return;
    }

    const reasonPropertyDeclaration = buildPropertyDefinitionText('flex-wrap', 'nowrap');
    const affectedPropertyDeclarationCode = buildPropertyName('align-content');

    return new Hint(
        i18nString(UIStrings.ruleViolatedBySameElementRuleReason, {
          REASON_PROPERTY_DECLARATION_CODE: reasonPropertyDeclaration,
          AFFECTED_PROPERTY_DECLARATION_CODE: affectedPropertyDeclarationCode,
        }),
        i18nString(UIStrings.ruleViolatedBySameElementRuleFix, {
          PROPERTY_NAME: buildPropertyName('flex-wrap'),
          PROPERTY_VALUE: buildPropertyValue('nowrap'),
        }),
    );
  }
}

export class FlexItemValidator extends CSSRuleValidator {
  constructor() {
    super(['flex', 'flex-basis', 'flex-grow', 'flex-shrink']);
  }

  getHint(propertyName: string, _computedStyles?: Map<string, string>, parentComputedStyles?: Map<string, string>): Hint
      |undefined {
    if (!parentComputedStyles) {
      return;
    }
    if (isFlexContainer(parentComputedStyles)) {
      return;
    }
    const reasonPropertyDeclaration = buildPropertyDefinitionText('display', parentComputedStyles?.get('display'));
    const affectedPropertyDeclarationCode = buildPropertyName(propertyName);
    const targetParentPropertyDeclaration = buildPropertyDefinitionText('display', 'flex');
    return new Hint(
        i18nString(UIStrings.ruleViolatedByParentElementRuleReason, {
          REASON_PROPERTY_DECLARATION_CODE: reasonPropertyDeclaration,
          AFFECTED_PROPERTY_DECLARATION_CODE: affectedPropertyDeclarationCode,
        }),
        i18nString(UIStrings.ruleViolatedByParentElementRuleFix, {
          EXISTING_PARENT_ELEMENT_RULE: reasonPropertyDeclaration,
          TARGET_PARENT_ELEMENT_RULE: targetParentPropertyDeclaration,
        }),
    );
  }
}

export class FlexContainerValidator extends CSSRuleValidator {
  constructor() {
    super(['flex-direction', 'flex-flow', 'flex-wrap']);
  }

  getHint(propertyName: string, computedStyles?: Map<string, string>): Hint|undefined {
    if (!computedStyles) {
      return;
    }
    if (isFlexContainer(computedStyles)) {
      return;
    }
    const reasonPropertyDeclaration = buildPropertyDefinitionText('display', computedStyles?.get('display'));
    const targetRuleCode = buildPropertyDefinitionText('display', 'flex');
    const affectedPropertyDeclarationCode = buildPropertyName(propertyName);

    return new Hint(
        i18nString(UIStrings.ruleViolatedBySameElementRuleReason, {
          REASON_PROPERTY_DECLARATION_CODE: reasonPropertyDeclaration,
          AFFECTED_PROPERTY_DECLARATION_CODE: affectedPropertyDeclarationCode,
        }),
        i18nString(UIStrings.ruleViolatedBySameElementRuleChangeSuggestion, {
          EXISTING_PROPERTY_DECLARATION: reasonPropertyDeclaration,
          TARGET_PROPERTY_DECLARATION: targetRuleCode,
        }),
    );
  }
}

export class GridContainerValidator extends CSSRuleValidator {
  constructor() {
    super([
      'grid',
      'grid-auto-columns',
      'grid-auto-flow',
      'grid-auto-rows',
      'grid-template',
      'grid-template-areas',
      'grid-template-columns',
      'grid-template-rows',
    ]);
  }

  getHint(propertyName: string, computedStyles?: Map<string, string>): Hint|undefined {
    if (isGridContainer(computedStyles) || isGridLanesContainer(computedStyles)) {
      return;
    }
    const reasonPropertyDeclaration = buildPropertyDefinitionText('display', computedStyles?.get('display'));
    const targetRuleCode = buildPropertyDefinitionText('display', 'grid');
    const affectedPropertyDeclarationCode = buildPropertyName(propertyName);

    return new Hint(
        i18nString(UIStrings.ruleViolatedBySameElementRuleReason, {
          REASON_PROPERTY_DECLARATION_CODE: reasonPropertyDeclaration,
          AFFECTED_PROPERTY_DECLARATION_CODE: affectedPropertyDeclarationCode,
        }),
        i18nString(UIStrings.ruleViolatedBySameElementRuleChangeSuggestion, {
          EXISTING_PROPERTY_DECLARATION: reasonPropertyDeclaration,
          TARGET_PROPERTY_DECLARATION: targetRuleCode,
        }),
    );
  }
}

export class GridItemValidator extends CSSRuleValidator {
  constructor() {
    super([
      'grid-area',
      'grid-column',
      'grid-row',
      'grid-row-end',
      'grid-row-start',
    ]);
  }

  getHint(propertyName: string, _computedStyles?: Map<string, string>, parentComputedStyles?: Map<string, string>): Hint
      |undefined {
    if (!parentComputedStyles) {
      return;
    }
    if (isGridContainer(parentComputedStyles) || isGridLanesContainer(parentComputedStyles)) {
      return;
    }
    const reasonPropertyDeclaration = buildPropertyDefinitionText('display', parentComputedStyles?.get('display'));
    const targetParentPropertyDeclaration = buildPropertyDefinitionText('display', 'grid');
    const affectedPropertyDeclarationCode = buildPropertyName(propertyName);

    return new Hint(
        i18nString(UIStrings.ruleViolatedByParentElementRuleReason, {
          REASON_PROPERTY_DECLARATION_CODE: reasonPropertyDeclaration,
          AFFECTED_PROPERTY_DECLARATION_CODE: affectedPropertyDeclarationCode,
        }),
        i18nString(UIStrings.ruleViolatedByParentElementRuleFix, {
          EXISTING_PARENT_ELEMENT_RULE: reasonPropertyDeclaration,
          TARGET_PARENT_ELEMENT_RULE: targetParentPropertyDeclaration,
        }),
    );
  }
}

export class FlexOrGridItemValidator extends CSSRuleValidator {
  constructor() {
    super([
      'order',
    ]);
  }

  getHint(propertyName: string, _computedStyles?: Map<string, string>, parentComputedStyles?: Map<string, string>): Hint
      |undefined {
    if (!parentComputedStyles) {
      return;
    }
    if (isFlexContainer(parentComputedStyles) || isGridContainer(parentComputedStyles)) {
      return;
    }
    const reasonPropertyDeclaration = buildPropertyDefinitionText('display', parentComputedStyles?.get('display'));
    const targetParentPropertyDeclaration =
        `${buildPropertyDefinitionText('display', 'flex')} or ${buildPropertyDefinitionText('display', 'grid')}`;
    const affectedPropertyDeclarationCode = buildPropertyName(propertyName);

    return new Hint(
        i18nString(UIStrings.ruleViolatedByParentElementRuleReason, {
          REASON_PROPERTY_DECLARATION_CODE: reasonPropertyDeclaration,
          AFFECTED_PROPERTY_DECLARATION_CODE: affectedPropertyDeclarationCode,
        }),
        i18nString(UIStrings.ruleViolatedByParentElementRuleFix, {
          EXISTING_PARENT_ELEMENT_RULE: reasonPropertyDeclaration,
          TARGET_PARENT_ELEMENT_RULE: targetParentPropertyDeclaration,
        }),
    );
  }
}

export class FlexGridValidator extends CSSRuleValidator {
  constructor() {
    // justify-content is specified to affect multicol, but we don't implement that yet.
    super(['justify-content']);
  }

  getHint(propertyName: string, computedStyles?: Map<string, string>, parentComputedStyles?: Map<string, string>): Hint
      |undefined {
    if (!computedStyles) {
      return;
    }

    if (isFlexContainer(computedStyles) || isGridContainer(computedStyles) || isGridLanesContainer(computedStyles)) {
      return;
    }

    if (parentComputedStyles &&
        (isFlexContainer(parentComputedStyles) || isGridContainer(parentComputedStyles) ||
         isGridLanesContainer(parentComputedStyles))) {
      const reasonContainerDisplayName = buildPropertyValue(parentComputedStyles.get('display') as string);
      const reasonPropertyName = buildPropertyName(propertyName);
      const reasonAlternativePropertyName = buildPropertyName('justify-self');
      return new Hint(
          i18nString(UIStrings.flexGridContainerPropertyRuleReason, {
            CONTAINER_DISPLAY_NAME: reasonContainerDisplayName,
            PROPERTY_NAME: reasonPropertyName,
          }),
          i18nString(UIStrings.flexGridContainerPropertyRuleFix, {
            PROPERTY_NAME: reasonPropertyName,
            ALTERNATIVE_PROPERTY_NAME: reasonAlternativePropertyName,
          }),
      );
    }

    const reasonPropertyDeclaration = buildPropertyDefinitionText('display', computedStyles.get('display'));
    const affectedPropertyDeclarationCode = buildPropertyName(propertyName);

    return new Hint(
        i18nString(UIStrings.ruleViolatedBySameElementRuleReason, {
          REASON_PROPERTY_DECLARATION_CODE: reasonPropertyDeclaration,
          AFFECTED_PROPERTY_DECLARATION_CODE: affectedPropertyDeclarationCode,
        }),
        i18nString(UIStrings.ruleViolatedBySameElementRuleChangeFlexOrGrid, {
          DISPLAY_GRID_RULE: buildPropertyDefinitionText('display', 'grid'),
          DISPLAY_FLEX_RULE: buildPropertyDefinitionText('display', 'flex'),
        }),
    );
  }
}

export class MulticolFlexGridValidator extends CSSRuleValidator {
  constructor() {
    super([
      'gap',
      'column-gap',
      'row-gap',
      'grid-gap',
      'grid-column-gap',
      'grid-row-gap',
    ]);
  }

  getHint(propertyName: string, computedStyles?: Map<string, string>): Hint|undefined {
    if (!computedStyles) {
      return;
    }

    if (isMulticolContainer(computedStyles) || isFlexContainer(computedStyles) || isGridContainer(computedStyles) ||
        isGridLanesContainer(computedStyles)) {
      return;
    }

    const reasonPropertyDeclaration = buildPropertyDefinitionText('display', computedStyles?.get('display'));
    const affectedPropertyDeclarationCode = buildPropertyName(propertyName);

    return new Hint(
        i18nString(UIStrings.ruleViolatedBySameElementRuleReason, {
          REASON_PROPERTY_DECLARATION_CODE: reasonPropertyDeclaration,
          AFFECTED_PROPERTY_DECLARATION_CODE: affectedPropertyDeclarationCode,
        }),
        i18nString(UIStrings.ruleViolatedBySameElementRuleFix, {
          PROPERTY_NAME: buildPropertyName('display'),
          PROPERTY_VALUE: buildPropertyValue(computedStyles?.get('display') as string),
        }),
    );
  }
}

export class PaddingValidator extends CSSRuleValidator {
  constructor() {
    super([
      'padding',
      'padding-top',
      'padding-right',
      'padding-bottom',
      'padding-left',
    ]);
  }

  getHint(propertyName: string, computedStyles?: Map<string, string>): Hint|undefined {
    const display = computedStyles?.get('display');
    if (!display) {
      return;
    }
    const tableAttributes = [
      'table-row-group',
      'table-header-group',
      'table-footer-group',
      'table-row',
      'table-column-group',
      'table-column',
    ];
    if (!tableAttributes.includes(display)) {
      return;
    }

    const reasonPropertyDeclaration = buildPropertyDefinitionText('display', computedStyles?.get('display'));
    const affectedPropertyDeclarationCode = buildPropertyName(propertyName);

    return new Hint(
        i18nString(UIStrings.ruleViolatedBySameElementRuleReason, {
          REASON_PROPERTY_DECLARATION_CODE: reasonPropertyDeclaration,
          AFFECTED_PROPERTY_DECLARATION_CODE: affectedPropertyDeclarationCode,
        }),
        i18nString(UIStrings.ruleViolatedBySameElementRuleFix, {
          PROPERTY_NAME: buildPropertyName('display'),
          PROPERTY_VALUE: buildPropertyValue(computedStyles?.get('display') as string),
        }),
    );
  }
}

export class PositionValidator extends CSSRuleValidator {
  constructor() {
    super([
      'top',
      'right',
      'bottom',
      'left',
    ]);
  }

  getHint(propertyName: string, computedStyles?: Map<string, string>): Hint|undefined {
    const position = computedStyles?.get('position');
    if (!position) {
      return;
    }
    if (position !== 'static') {
      return;
    }

    const reasonPropertyDeclaration = buildPropertyDefinitionText('position', computedStyles?.get('position'));
    const affectedPropertyDeclarationCode = buildPropertyName(propertyName);

    return new Hint(
        i18nString(UIStrings.ruleViolatedBySameElementRuleReason, {
          REASON_PROPERTY_DECLARATION_CODE: reasonPropertyDeclaration,
          AFFECTED_PROPERTY_DECLARATION_CODE: affectedPropertyDeclarationCode,
        }),
        i18nString(UIStrings.ruleViolatedBySameElementRuleFix, {
          PROPERTY_NAME: buildPropertyName('position'),
          PROPERTY_VALUE: buildPropertyValue(computedStyles?.get('position') as string),
        }),
    );
  }
}

export class ZIndexValidator extends CSSRuleValidator {
  constructor() {
    super([
      'z-index',
    ]);
  }

  getHint(propertyName: string, computedStyles?: Map<string, string>, parentComputedStyles?: Map<string, string>): Hint
      |undefined {
    const position = computedStyles?.get('position');
    if (!position) {
      return;
    }
    if (['absolute', 'relative', 'fixed', 'sticky'].includes(position) || isFlexContainer(parentComputedStyles) ||
        isGridContainer(parentComputedStyles)) {
      return;
    }

    const reasonPropertyDeclaration = buildPropertyDefinitionText('position', computedStyles?.get('position'));
    const affectedPropertyDeclarationCode = buildPropertyName(propertyName);

    return new Hint(
        i18nString(UIStrings.ruleViolatedBySameElementRuleReason, {
          REASON_PROPERTY_DECLARATION_CODE: reasonPropertyDeclaration,
          AFFECTED_PROPERTY_DECLARATION_CODE: affectedPropertyDeclarationCode,
        }),
        i18nString(UIStrings.ruleViolatedBySameElementRuleFix, {
          PROPERTY_NAME: buildPropertyName('position'),
          PROPERTY_VALUE: buildPropertyValue(computedStyles?.get('position') as string),
        }),
    );
  }
}

export class PositionAnchorValidator extends CSSRuleValidator {
  constructor() {
    super(['position-anchor']);
  }

  override getHint(propertyName: string, computedStyles?: Map<string, string>): Hint|undefined {
    const position = computedStyles?.get('position') ?? 'static';
    const display = computedStyles?.get('display');

    if (position !== 'absolute' && position !== 'fixed') {
      return new Hint(
          i18nString(UIStrings.invalidAnchorPositioning, {POSITION: position}),
          i18nString(UIStrings.invalidAnchorPositioningFix));
    }

    if (display === 'none') {
      return new Hint(i18nString(UIStrings.unusedAnchorPositioning, {POSITION: position}), null);
    }

    return undefined;
  }
}

/**
 * Validates if CSS width/height are having an effect on an element.
 * See "Applies to" in https://www.w3.org/TR/css-sizing-3/#propdef-width.
 * See "Applies to" in https://www.w3.org/TR/css-sizing-3/#propdef-height.
 */
export class SizingValidator extends CSSRuleValidator {
  constructor() {
    super([
      'width',
      'height',
    ]);
  }

  getHint(
      propertyName: string, computedStyles?: Map<string, string>, _parentComputedStyles?: Map<string, string>,
      nodeName?: string): Hint|undefined {
    if (!computedStyles || !nodeName) {
      return;
    }
    if (!isInlineElement(computedStyles)) {
      return;
    }
    // See https://html.spec.whatwg.org/multipage/rendering.html#replaced-elements.
    if (isPossiblyReplacedElement(nodeName)) {
      return;
    }

    const reasonPropertyDeclaration = buildPropertyDefinitionText('display', computedStyles?.get('display'));
    const affectedPropertyDeclarationCode = buildPropertyName(propertyName);

    return new Hint(
        i18nString(UIStrings.ruleViolatedBySameElementRuleReason, {
          REASON_PROPERTY_DECLARATION_CODE: reasonPropertyDeclaration,
          AFFECTED_PROPERTY_DECLARATION_CODE: affectedPropertyDeclarationCode,
        }),
        i18nString(UIStrings.ruleViolatedBySameElementRuleFix, {
          PROPERTY_NAME: buildPropertyName('display'),
          PROPERTY_VALUE: buildPropertyValue(computedStyles?.get('display') as string),
        }),
    );
  }
}

/**
 * Checks that font variation settings are applicable to the actual font.
 */
export class FontVariationSettingsValidator extends CSSRuleValidator {
  constructor() {
    super([
      'font-variation-settings',
    ]);
  }

  getHint(
      _propertyName: string, computedStyles?: Map<string, string>, _parentComputedStyles?: Map<string, string>,
      _nodeName?: string, fontFaces?: SDK.CSSFontFace.CSSFontFace[]): Hint|undefined {
    if (!computedStyles) {
      return;
    }
    const value = computedStyles.get('font-variation-settings');
    if (!value) {
      return;
    }
    const fontFamily = computedStyles.get('font-family');
    if (!fontFamily) {
      return;
    }
    const fontFamilies = new Set<string>(SDK.CSSPropertyParser.parseFontFamily(fontFamily));
    const matchingFontFaces = (fontFaces || []).filter(f => fontFamilies.has(f.getFontFamily()));
    const variationSettings = SDK.CSSPropertyParser.parseFontVariationSettings(value);
    const warnings = [];
    for (const elementSetting of variationSettings) {
      for (const font of matchingFontFaces) {
        const fontSetting = font.getVariationAxisByTag(elementSetting.tag);
        if (!fontSetting) {
          continue;
        }
        if (elementSetting.value < fontSetting.minValue || elementSetting.value > fontSetting.maxValue) {
          warnings.push(i18nString(UIStrings.fontVariationSettingsWarning, {
            PH1: elementSetting.tag,
            PH2: elementSetting.value,
            PH3: fontSetting.minValue,
            PH4: fontSetting.maxValue,
            PH5: font.getFontFamily(),
          }));
        }
      }
    }

    if (!warnings.length) {
      return;
    }

    return new Hint(
        warnings.join(' '),
        '',
    );
  }
}

const CSS_RULE_VALIDATORS = [
  AlignContentValidator,
  FlexContainerValidator,
  FlexGridValidator,
  FlexItemValidator,
  FlexOrGridItemValidator,
  FontVariationSettingsValidator,
  GridContainerValidator,
  GridItemValidator,
  MulticolFlexGridValidator,
  PaddingValidator,
  PositionValidator,
  PositionAnchorValidator,
  SizingValidator,
  ZIndexValidator,
];

const setupCSSRulesValidators = (): Map<string, CSSRuleValidator[]> => {
  const validatorsMap = new Map<string, CSSRuleValidator[]>();
  for (const validatorClass of CSS_RULE_VALIDATORS) {
    const validator = new validatorClass();
    const affectedProperties = validator.getApplicableProperties();

    for (const affectedProperty of affectedProperties) {
      let propertyValidators = validatorsMap.get(affectedProperty);
      if (propertyValidators === undefined) {
        propertyValidators = [];
      }
      propertyValidators.push(validator);

      validatorsMap.set(affectedProperty, propertyValidators);
    }
  }
  return validatorsMap;
};

export const cssRuleValidatorsMap: Map<string, CSSRuleValidator[]> = setupCSSRulesValidators();
