/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

import {
  ChangeDetectionStrategy,
  Component,
  EventEmitter,
  Input,
  Output,
  ViewEncapsulation,
  OnInit,
  inject,
  booleanAttribute,
} from '@angular/core';
import {MatChip} from './chip';
import {MAT_CHIP, MAT_CHIPS_DEFAULT_OPTIONS} from './tokens';
import {MatChipAction} from './chip-action';

/** Event object emitted by MatChipOption when selected or deselected. */
export class MatChipSelectionChange {
  constructor(
    /** Reference to the chip that emitted the event. */
    public source: MatChipOption,
    /** Whether the chip that emitted the event is selected. */
    public selected: boolean,
    /** Whether the selection change was a result of a user interaction. */
    public isUserInput = false,
  ) {}
}

/**
 * An extension of the MatChip component that supports chip selection. Used with MatChipListbox.
 *
 * Unlike other chips, the user can focus on disabled chip options inside a MatChipListbox. The
 * user cannot click disabled chips.
 */
@Component({
  selector: 'mat-basic-chip-option, [mat-basic-chip-option], mat-chip-option, [mat-chip-option]',
  templateUrl: 'chip-option.html',
  styleUrl: 'chip.css',
  host: {
    'class': 'mat-mdc-chip mat-mdc-chip-option',
    '[class.mdc-evolution-chip]': '!_isBasicChip',
    '[class.mdc-evolution-chip--filter]': '!_isBasicChip',
    '[class.mdc-evolution-chip--selectable]': '!_isBasicChip',
    '[class.mat-mdc-chip-selected]': 'selected',
    '[class.mat-mdc-chip-multiple]': '_chipListMultiple',
    '[class.mat-mdc-chip-disabled]': 'disabled',
    '[class.mat-mdc-chip-with-avatar]': 'leadingIcon',
    '[class.mdc-evolution-chip--disabled]': 'disabled',
    '[class.mdc-evolution-chip--selected]': 'selected',
    // This class enables the transition on the checkmark. Usually MDC adds it when selection
    // starts and removes it once the animation is finished. We don't need to go through all
    // the trouble, because we only care about the selection animation. MDC needs to do it,
    // because they also have an exit animation that we don't care about.
    '[class.mdc-evolution-chip--selecting]': '!_animationsDisabled',
    '[class.mdc-evolution-chip--with-trailing-action]': '_hasTrailingIcon()',
    '[class.mdc-evolution-chip--with-primary-icon]': 'leadingIcon',
    '[class.mdc-evolution-chip--with-primary-graphic]': '_hasLeadingGraphic()',
    '[class.mdc-evolution-chip--with-avatar]': 'leadingIcon',
    '[class.mat-mdc-chip-highlighted]': 'highlighted',
    '[class.mat-mdc-chip-with-trailing-icon]': '_hasTrailingIcon()',
    '[attr.tabindex]': 'null',
    '[attr.aria-label]': 'null',
    '[attr.aria-description]': 'null',
    '[attr.role]': 'role',
    '[id]': 'id',
  },
  providers: [
    {provide: MatChip, useExisting: MatChipOption},
    {provide: MAT_CHIP, useExisting: MatChipOption},
  ],
  encapsulation: ViewEncapsulation.None,
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [MatChipAction],
})
export class MatChipOption extends MatChip implements OnInit {
  /** Default chip options. */
  private _defaultOptions = inject(MAT_CHIPS_DEFAULT_OPTIONS, {optional: true});

  /** Whether the chip list is selectable. */
  chipListSelectable: boolean = true;

  /** Whether the chip list is in multi-selection mode. */
  _chipListMultiple: boolean = false;

  /** Whether the chip list hides single-selection indicator. */
  _chipListHideSingleSelectionIndicator: boolean =
    this._defaultOptions?.hideSingleSelectionIndicator ?? false;

  /**
   * Whether or not the chip is selectable.
   *
   * When a chip is not selectable, changes to its selected state are always
   * ignored. By default an option chip is selectable, and it becomes
   * non-selectable if its parent chip list is not selectable.
   */
  @Input({transform: booleanAttribute})
  get selectable(): boolean {
    return this._selectable && this.chipListSelectable;
  }
  set selectable(value: boolean) {
    this._selectable = value;
    this._changeDetectorRef.markForCheck();
  }
  protected _selectable: boolean = true;

  /** Whether the chip is selected. */
  @Input({transform: booleanAttribute})
  get selected(): boolean {
    return this._selected;
  }
  set selected(value: boolean) {
    this._setSelectedState(value, false, true);
  }
  private _selected = false;

  /**
   * The ARIA selected applied to the chip. Conforms to WAI ARIA best practices for listbox
   * interaction patterns.
   *
   * From [WAI ARIA Listbox authoring practices guide](
   * https://www.w3.org/WAI/ARIA/apg/patterns/listbox/):
   *  "If any options are selected, each selected option has either aria-selected or aria-checked
   *  set to true. All options that are selectable but not selected have either aria-selected or
   *  aria-checked set to false."
   *
   * Set `aria-selected="false"` on not-selected listbox options that are selectable to fix
   * VoiceOver reading every option as "selected" (#25736).
   */
  get ariaSelected(): string | null {
    return this.selectable ? this.selected.toString() : null;
  }

  /** The unstyled chip selector for this component. */
  protected override basicChipAttrName = 'mat-basic-chip-option';

  /** Emitted when the chip is selected or deselected. */
  @Output() readonly selectionChange: EventEmitter<MatChipSelectionChange> =
    new EventEmitter<MatChipSelectionChange>();

  override ngOnInit() {
    super.ngOnInit();
    this.role = 'presentation';
  }

  /** Selects the chip. */
  select(): void {
    this._setSelectedState(true, false, true);
  }

  /** Deselects the chip. */
  deselect(): void {
    this._setSelectedState(false, false, true);
  }

  /** Selects this chip and emits userInputSelection event */
  selectViaInteraction(): void {
    this._setSelectedState(true, true, true);
  }

  /** Toggles the current selected state of this chip. */
  toggleSelected(isUserInput: boolean = false): boolean {
    this._setSelectedState(!this.selected, isUserInput, true);
    return this.selected;
  }

  override _handlePrimaryActionInteraction() {
    if (!this.disabled) {
      // Interacting with the primary action implies that the chip already has focus, however
      // there's a bug in Safari where focus ends up lingering on the previous chip (see #27544).
      // We work around it by explicitly focusing the primary action of the current chip.
      this.focus();

      if (this.selectable) {
        this.toggleSelected(true);
      }
    }
  }

  _hasLeadingGraphic() {
    if (this.leadingIcon) {
      return true;
    }

    // The checkmark graphic communicates selected state for both single-select and multi-select.
    // Include checkmark in single-select to fix a11y issue where selected state is communicated
    // visually only using color (#25886).
    return !this._chipListHideSingleSelectionIndicator || this._chipListMultiple;
  }

  _setSelectedState(isSelected: boolean, isUserInput: boolean, emitEvent: boolean) {
    if (isSelected !== this.selected) {
      this._selected = isSelected;

      if (emitEvent) {
        this.selectionChange.emit({
          source: this,
          isUserInput,
          selected: this.selected,
        });
      }

      this._changeDetectorRef.markForCheck();
    }
  }
}
