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

import type * as Protocol from '../../generated/protocol.js';
import * as TextUtils from '../../models/text_utils/text_utils.js';

import {cssMetadata} from './CSSMetadata.js';
import type {CSSModel, Edit} from './CSSModel.js';
import {CSSProperty} from './CSSProperty.js';
import type {CSSRule} from './CSSRule.js';
import type {Target} from './Target.js';

export class CSSStyleDeclaration {
  readonly #cssModel: CSSModel;
  parentRule: CSSRule|null;
  #allProperties: CSSProperty[] = [];
  styleSheetId?: Protocol.DOM.StyleSheetId;
  range: TextUtils.TextRange.TextRange|null = null;
  cssText?: string;
  #shorthandValues = new Map<string, string>();
  #shorthandIsImportant = new Set<string>();
  #activePropertyMap = new Map<string, CSSProperty>();
  #leadingProperties: CSSProperty[]|null = null;
  type: Type;
  // For CSSStyles coming from animations,
  // This holds the name of the animation.
  #animationName?: string;
  constructor(
      cssModel: CSSModel, parentRule: CSSRule|null, payload: Protocol.CSS.CSSStyle, type: Type,
      animationName?: string) {
    this.#cssModel = cssModel;
    this.parentRule = parentRule;
    this.#reinitialize(payload);
    this.type = type;
    this.#animationName = animationName;
  }

  rebase(edit: Edit): void {
    if (this.styleSheetId !== edit.styleSheetId || !this.range) {
      return;
    }
    if (edit.oldRange.equal(this.range)) {
      this.#reinitialize((edit.payload as Protocol.CSS.CSSStyle));
    } else {
      this.range = this.range.rebaseAfterTextEdit(edit.oldRange, edit.newRange);
      for (let i = 0; i < this.#allProperties.length; ++i) {
        this.#allProperties[i].rebase(edit);
      }
    }
  }

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

  #reinitialize(payload: Protocol.CSS.CSSStyle): void {
    this.styleSheetId = payload.styleSheetId;
    this.range = payload.range ? TextUtils.TextRange.TextRange.fromObject(payload.range) : null;

    const shorthandEntries = payload.shorthandEntries;
    this.#shorthandValues = new Map();
    this.#shorthandIsImportant = new Set();
    for (let i = 0; i < shorthandEntries.length; ++i) {
      this.#shorthandValues.set(shorthandEntries[i].name, shorthandEntries[i].value);
      if (shorthandEntries[i].important) {
        this.#shorthandIsImportant.add(shorthandEntries[i].name);
      }
    }

    this.#allProperties = [];

    if (payload.cssText && this.range) {
      const longhands = [];
      for (const cssProperty of payload.cssProperties) {
        const range = cssProperty.range;
        if (!range) {
          continue;
        }
        const parsedProperty = CSSProperty.parsePayload(this, this.#allProperties.length, cssProperty);
        this.#allProperties.push(parsedProperty);
        for (const longhand of parsedProperty.getLonghandProperties()) {
          longhands.push(longhand);
        }
      }
      for (const longhand of longhands) {
        longhand.index = this.#allProperties.length;
        this.#allProperties.push(longhand);
      }
    } else {
      for (const cssProperty of payload.cssProperties) {
        this.#allProperties.push(CSSProperty.parsePayload(this, this.#allProperties.length, cssProperty));
      }
    }

    this.#generateSyntheticPropertiesIfNeeded();
    this.#computeInactiveProperties();

    // TODO(changhaohan): verify if this #activePropertyMap is still necessary, or if it is
    // providing different information against the activeness in #allProperties.
    this.#activePropertyMap = new Map();
    for (const property of this.#allProperties) {
      if (!property.activeInStyle()) {
        continue;
      }
      this.#activePropertyMap.set(property.name, property);
    }

    this.cssText = payload.cssText;
    this.#leadingProperties = null;
  }

  #generateSyntheticPropertiesIfNeeded(): void {
    if (this.range) {
      return;
    }

    if (!this.#shorthandValues.size) {
      return;
    }

    const propertiesSet = new Set<string>();
    for (const property of this.#allProperties) {
      propertiesSet.add(property.name);
    }

    const generatedProperties = [];
    // For style-based properties, generate #shorthands with values when possible.
    for (const property of this.#allProperties) {
      // For style-based properties, try generating #shorthands.
      const shorthands = cssMetadata().getShorthands(property.name) || [];
      for (const shorthand of shorthands) {
        if (propertiesSet.has(shorthand)) {
          continue;
        }  // There already is a shorthand this #longhand falls under.
        const shorthandValue = this.#shorthandValues.get(shorthand);
        if (!shorthandValue) {
          continue;
        }  // Never generate synthetic #shorthands when no value is available.

        // Generate synthetic shorthand we have a value for.
        const shorthandImportance = Boolean(this.#shorthandIsImportant.has(shorthand));
        const shorthandProperty = new CSSProperty(
            this, this.allProperties().length, shorthand, shorthandValue, shorthandImportance, false, true, false);
        generatedProperties.push(shorthandProperty);
        propertiesSet.add(shorthand);
      }
    }
    this.#allProperties = this.#allProperties.concat(generatedProperties);
  }

  #computeLeadingProperties(): CSSProperty[] {
    function propertyHasRange(property: CSSProperty): boolean {
      return Boolean(property.range);
    }

    if (this.range) {
      return this.#allProperties.filter(propertyHasRange);
    }

    const leadingProperties = [];
    for (const property of this.#allProperties) {
      const shorthands = cssMetadata().getShorthands(property.name) || [];
      let belongToAnyShorthand = false;
      for (const shorthand of shorthands) {
        if (this.#shorthandValues.get(shorthand)) {
          belongToAnyShorthand = true;
          break;
        }
      }
      if (!belongToAnyShorthand) {
        leadingProperties.push(property);
      }
    }

    return leadingProperties;
  }

  leadingProperties(): CSSProperty[] {
    if (!this.#leadingProperties) {
      this.#leadingProperties = this.#computeLeadingProperties();
    }
    return this.#leadingProperties;
  }

  target(): Target {
    return this.#cssModel.target();
  }

  cssModel(): CSSModel {
    return this.#cssModel;
  }

  #computeInactiveProperties(): void {
    const activeProperties = new Map<string, CSSProperty>();
    // The order of the properties are:
    // 1. regular property, including shorthands
    // 2. longhand components from shorthands, in the order of their shorthands.
    const processedLonghands = new Set();
    for (const property of this.#allProperties) {
      const metadata = cssMetadata();
      const canonicalName = metadata.canonicalPropertyName(property.name);
      if (property.disabled || !property.parsedOk) {
        if (!property.disabled && metadata.isCustomProperty(property.name)) {
          // Variable declarations that aren't parsedOk still "overload" other previous active declarations.
          activeProperties.get(canonicalName)?.setActive(false);
          activeProperties.delete(canonicalName);
        }
        property.setActive(false);
        continue;
      }
      if (processedLonghands.has(property)) {
        continue;
      }
      for (const longhand of property.getLonghandProperties()) {
        const activeLonghand = activeProperties.get(longhand.name);
        if (!activeLonghand) {
          activeProperties.set(longhand.name, longhand);
        } else if (!activeLonghand.important || longhand.important) {
          activeLonghand.setActive(false);
          activeProperties.set(longhand.name, longhand);
        } else {
          longhand.setActive(false);
        }
        processedLonghands.add(longhand);
      }

      const activeProperty = activeProperties.get(canonicalName);
      if (!activeProperty) {
        activeProperties.set(canonicalName, property);
      } else if (!activeProperty.important || property.important) {
        activeProperty.setActive(false);
        activeProperties.set(canonicalName, property);
      } else {
        property.setActive(false);
      }
    }
  }

  allProperties(): CSSProperty[] {
    return this.#allProperties;
  }

  hasActiveProperty(name: string): boolean {
    return this.#activePropertyMap.has(name);
  }

  getPropertyValue(name: string): string {
    const property = this.#activePropertyMap.get(name);
    return property ? property.value : '';
  }

  isPropertyImplicit(name: string): boolean {
    const property = this.#activePropertyMap.get(name);
    return property ? property.implicit : false;
  }

  propertyAt(index: number): CSSProperty|null {
    return (index < this.allProperties().length) ? this.allProperties()[index] : null;
  }

  pastLastSourcePropertyIndex(): number {
    for (let i = this.allProperties().length - 1; i >= 0; --i) {
      if (this.allProperties()[i].range) {
        return i + 1;
      }
    }
    return 0;
  }

  #insertionRange(index: number): TextUtils.TextRange.TextRange {
    const property = this.propertyAt(index);
    if (property?.range) {
      return property.range.collapseToStart();
    }
    if (!this.range) {
      throw new Error('CSSStyleDeclaration.range is null');
    }
    return this.range.collapseToEnd();
  }

  newBlankProperty(index?: number): CSSProperty {
    index = (typeof index === 'undefined') ? this.pastLastSourcePropertyIndex() : index;
    const property = new CSSProperty(this, index, '', '', false, false, true, false, '', this.#insertionRange(index));
    return property;
  }

  setText(text: string, majorChange: boolean): Promise<boolean> {
    if (!this.range || !this.styleSheetId) {
      return Promise.resolve(false);
    }
    return this.#cssModel.setStyleText(this.styleSheetId, this.range, text, majorChange);
  }

  insertPropertyAt(index: number, name: string, value: string, userCallback?: ((arg0: boolean) => void)): void {
    void this.newBlankProperty(index).setText(name + ': ' + value + ';', false, true).then(userCallback);
  }

  appendProperty(name: string, value: string, userCallback?: ((arg0: boolean) => void)): void {
    this.insertPropertyAt(this.allProperties().length, name, value, userCallback);
  }
}

export enum Type {
  /* eslint-disable @typescript-eslint/naming-convention -- Used by web_tests. */
  Regular = 'Regular',
  Inline = 'Inline',
  Attributes = 'Attributes',
  Pseudo = 'Pseudo',  // This type is for style declarations generated by devtools
  Transition = 'Transition',
  Animation = 'Animation',
  /* eslint-enable @typescript-eslint/naming-convention */
}
