import {
  ChangeDetectionStrategy,
  ChangeDetectorRef,
  Component,
  ElementRef,
  EventEmitter,
  Input,
  OnChanges,
  Output,
  SimpleChanges,
  ViewEncapsulation,
} from '@angular/core';
import { CrtInput, CrtOutput, CrtViewElement } from '@creatio-devkit/common';

import { RATING_SELECTOR, RATING_TYPE } from '../../runtime-feature.ids';

const MIN_STAR_COUNT = 1;
const MAX_STAR_COUNT = 10;
const DEFAULT_STAR_COUNT = 5;
const DEFAULT_RATING_VALUE = 0;
const DEFAULT_SIZE: 'small' | 'medium' | 'large' = 'medium';
const DEFAULT_ACTIVE_COLOR = '#FFC107';
const DEFAULT_INACTIVE_COLOR = '#D1D5DB';

const FILLED_STAR_GLYPH = '★';
const EMPTY_STAR_GLYPH = '☆';

@CrtViewElement({
  selector: RATING_SELECTOR,
  type: RATING_TYPE,
})
@Component({
  selector: RATING_SELECTOR,
  standalone: false,
  templateUrl: './rating.component.html',
  styleUrls: ['./rating.component.scss'],
  encapsulation: ViewEncapsulation.ShadowDom,
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class RatingComponent implements OnChanges {
  private _ratingValue = DEFAULT_RATING_VALUE;

  @Input()
  @CrtInput()
  public starCount = DEFAULT_STAR_COUNT;

  @Input()
  @CrtInput()
  public ratingValue = DEFAULT_RATING_VALUE;

  @Output()
  @CrtOutput()
  public readonly ratingValueChange = new EventEmitter<number>();

  @Input()
  @CrtInput()
  public size: 'small' | 'medium' | 'large' = DEFAULT_SIZE;

  @Input()
  @CrtInput()
  public activeColor = DEFAULT_ACTIVE_COLOR;

  @Input()
  @CrtInput()
  public inactiveColor = DEFAULT_INACTIVE_COLOR;

  @Input()
  @CrtInput()
  public isReadOnly = false;

  @Input()
  @CrtInput()
  public isDisabled = false;

  @Input()
  @CrtInput()
  public visible = true;

  public constructor(
    private readonly _elementRef: ElementRef<HTMLElement>,
    private readonly _cdr: ChangeDetectorRef,
  ) {}

  protected get stars(): number[] {
    return Array.from({ length: this._safeStarCount() }, (_value, index) => index + 1);
  }

  protected get focusableStar(): number {
    return this._ratingValue > 0 ? this._ratingValue : 1;
  }

  protected readonly filledGlyph = FILLED_STAR_GLYPH;
  protected readonly emptyGlyph = EMPTY_STAR_GLYPH;

  public ngOnChanges(changes: SimpleChanges): void {
    if (!changes['ratingValue'] && !changes['starCount']) {
      return;
    }

    const originalValue = this.ratingValue;
    const clampedValue = this._clamp(originalValue);
    this._ratingValue = clampedValue;
    this.ratingValue = clampedValue;

    if (clampedValue !== originalValue) {
      this.ratingValueChange.emit(clampedValue);
    }
  }

  protected onStarClick(star: number): void {
    this._setRating(star);
  }

  protected onKeydown(event: KeyboardEvent): void {
    if (this.isDisabled) {
      return;
    }

    let nextValue: number | null = null;
    switch (event.key) {
      case 'ArrowRight':
      case 'ArrowUp':
        nextValue = this._ratingValue + 1;
        break;
      case 'ArrowLeft':
      case 'ArrowDown':
        nextValue = this._ratingValue - 1;
        break;
      case 'Home':
        nextValue = 0;
        break;
      case 'End':
        nextValue = this._safeStarCount();
        break;
      default:
        return;
    }

    event.preventDefault();
    this._setRating(nextValue);
    this._focusStar(this.focusableStar);
  }

  private _setRating(next: number): void {
    if (this.isReadOnly || this.isDisabled) {
      return;
    }

    const clamped = this._clamp(next);
    if (clamped === this._ratingValue) {
      return;
    }

    this._ratingValue = clamped;
    this.ratingValue = clamped;
    this.ratingValueChange.emit(clamped);
    this._cdr.markForCheck();
  }

  private _focusStar(star: number): void {
    this._cdr.detectChanges();
    const host = this._elementRef.nativeElement;
    const button = host.shadowRoot?.querySelector<HTMLButtonElement>(`[data-star="${star}"]`);
    button?.focus();
  }

  private _safeStarCount(): number {
    const count = Math.trunc(this.starCount);
    return Number.isFinite(count) && count > 0
      ? Math.min(MAX_STAR_COUNT, Math.max(MIN_STAR_COUNT, count))
      : DEFAULT_STAR_COUNT;
  }

  private _clamp(value: number): number {
    const safeValue = Number.isFinite(value) ? Math.trunc(value) : 0;
    return Math.min(this._safeStarCount(), Math.max(0, safeValue));
  }
}
