/**
 * @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 {AnimationPlayer, ɵStyleDataMap} from '../../../../src/animations';

import {computeStyle} from '../../util';
import {SpecialCasedStyles} from '../special_cased_styles';

export class WebAnimationsPlayer implements AnimationPlayer {
  private _onDoneFns: Function[] = [];
  private _onStartFns: Function[] = [];
  private _onDestroyFns: Function[] = [];
  private _duration: number;
  private _delay: number;
  private _initialized = false;
  private _finished = false;
  private _started = false;
  private _destroyed = false;
  private _finalKeyframe?: ɵStyleDataMap;

  // the following original fns are persistent copies of the _onStartFns and _onDoneFns
  // and are used to reset the fns to their original values upon reset()
  // (since the _onStartFns and _onDoneFns get deleted after they are called)
  private _originalOnDoneFns: Function[] = [];
  private _originalOnStartFns: Function[] = [];

  public domPlayer: Animation | null = null;
  public time = 0;

  public parentPlayer: AnimationPlayer | null = null;
  public currentSnapshot: ɵStyleDataMap = new Map();

  constructor(
    public element: any,
    public keyframes: Array<ɵStyleDataMap>,
    public options: {[key: string]: string | number},
    private _specialStyles?: SpecialCasedStyles | null,
  ) {
    this._duration = <number>options['duration'];
    this._delay = <number>options['delay'] || 0;
    this.time = this._duration + this._delay;
  }

  private _onFinish() {
    if (!this._finished) {
      this._finished = true;
      this._onDoneFns.forEach((fn) => fn());
      this._onDoneFns = [];
    }
  }

  init(): void {
    if (!this._buildPlayer()) {
      return;
    }
    this._preparePlayerBeforeStart();
  }

  private _buildPlayer(): Animation | null {
    if (this._initialized) return this.domPlayer;
    this._initialized = true;

    const keyframes = this.keyframes;

    const animation = this._triggerWebAnimation(this.element, keyframes, this.options);
    if (!animation) {
      this._onFinish();
      return null;
    }

    this.domPlayer = animation;
    this._finalKeyframe = keyframes.length ? keyframes[keyframes.length - 1] : new Map();
    const onFinish = () => this._onFinish();
    animation.addEventListener('finish', onFinish);
    this.onDestroy(() => {
      // We must remove the `finish` event listener once an animation has completed all its
      // iterations. This action is necessary to prevent a memory leak since the listener captures
      // `this`, creating a closure that prevents `this` from being garbage collected.
      animation.removeEventListener('finish', onFinish);
    });
    return animation;
  }

  private _preparePlayerBeforeStart() {
    // this is required so that the player doesn't start to animate right away
    if (this._delay) {
      this._resetDomPlayerState();
    } else {
      this.domPlayer?.pause();
    }
  }

  private _convertKeyframesToObject(keyframes: Array<ɵStyleDataMap>): any[] {
    const kfs: any[] = [];
    keyframes.forEach((frame) => {
      kfs.push(Object.fromEntries(frame));
    });
    return kfs;
  }

  /** @internal */
  _triggerWebAnimation(
    element: HTMLElement,
    keyframes: Array<ɵStyleDataMap>,
    options: any,
  ): Animation | null {
    const keyframesObject = this._convertKeyframesToObject(keyframes);

    // Account for `Element.animate` throwing an exception (Firefox) or returning null (Chromium) in certain
    // conditions. See https://github.com/angular/angular/issues/64486
    try {
      return element.animate(keyframesObject, options);
    } catch {
      return null;
    }
  }

  onStart(fn: () => void): void {
    this._originalOnStartFns.push(fn);
    this._onStartFns.push(fn);
  }

  onDone(fn: () => void): void {
    this._originalOnDoneFns.push(fn);
    this._onDoneFns.push(fn);
  }

  onDestroy(fn: () => void): void {
    this._onDestroyFns.push(fn);
  }

  play(): void {
    const player = this._buildPlayer();
    if (!player) {
      return;
    }
    if (!this.hasStarted()) {
      this._onStartFns.forEach((fn) => fn());
      this._onStartFns = [];
      this._started = true;
      if (this._specialStyles) {
        this._specialStyles.start();
      }
    }
    player.play();
  }

  pause(): void {
    this.init();
    this.domPlayer?.pause();
  }

  finish(): void {
    this.init();
    if (!this.domPlayer) return;
    if (this._specialStyles) {
      this._specialStyles.finish();
    }
    this._onFinish();
    this.domPlayer.finish();
  }

  reset(): void {
    this._resetDomPlayerState();
    this._destroyed = false;
    this._finished = false;
    this._started = false;
    this._onStartFns = this._originalOnStartFns;
    this._onDoneFns = this._originalOnDoneFns;
  }

  private _resetDomPlayerState() {
    this.domPlayer?.cancel();
  }

  restart(): void {
    this.reset();
    this.play();
  }

  hasStarted(): boolean {
    return this._started;
  }

  destroy(): void {
    if (!this._destroyed) {
      this._destroyed = true;
      this._resetDomPlayerState();
      this._onFinish();
      if (this._specialStyles) {
        this._specialStyles.destroy();
      }
      this._onDestroyFns.forEach((fn) => fn());
      this._onDestroyFns = [];
    }
  }

  setPosition(p: number): void {
    if (!this.domPlayer) {
      this.init();
    }
    if (this.domPlayer) {
      this.domPlayer.currentTime = p * this.time;
    }
  }

  getPosition(): number {
    if (!this.domPlayer) {
      return this._initialized ? 1 : 0;
    }
    // tsc is complaining with TS2362 without the conversion to number
    return +(this.domPlayer.currentTime ?? 0) / this.time;
  }

  get totalTime(): number {
    return this._delay + this._duration;
  }

  beforeDestroy() {
    const styles: ɵStyleDataMap = new Map();
    if (this.hasStarted()) {
      // note: this code is invoked only when the `play` function was called prior to this
      // (thus `hasStarted` returns true), this implies that the code that initializes
      // `_finalKeyframe` has also been executed and the non-null assertion can be safely used here
      const finalKeyframe = this._finalKeyframe!;
      finalKeyframe.forEach((val, prop) => {
        if (prop !== 'offset') {
          styles.set(prop, this._finished ? val : computeStyle(this.element, prop));
        }
      });
    }

    this.currentSnapshot = styles;
  }

  /** @internal */
  triggerCallback(phaseName: string): void {
    const methods = phaseName === 'start' ? this._onStartFns : this._onDoneFns;
    methods.forEach((fn) => fn());
    methods.length = 0;
  }
}
