import {
  ChangeDetectionStrategy,
  Component,
  effect,
  ElementRef,
  input,
  output,
  viewChild,
} from '@angular/core';
import Hls from 'hls.js';

@Component({
  selector: 'app-hls-player',
  templateUrl: './hls-player.html',
  styleUrl: './hls-player.css',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class HlsPlayerComponent {
  readonly src = input.required<string>();
  readonly seekToSec = input<number | null>(null);
  readonly isPlaying = input<boolean | null>(null);
  readonly currentTimeSecChange = output<number>();
  readonly durationSecChange = output<number>();
  readonly playingChange = output<boolean>();

  private readonly videoRef = viewChild<ElementRef<HTMLVideoElement>>('videoEl');
  private pendingSeekSec: number | null = null;

  constructor() {
    effect((onCleanup) => {
      const url = this.src();
      const ref = this.videoRef();
      if (!ref) {
        return;
      }
      const video = ref.nativeElement;
      let hls: Hls | null = null;
      const emitCurrentTime = () => {
        this.currentTimeSecChange.emit(video.currentTime || 0);
      };
      const emitPlaying = () => {
        this.playingChange.emit(!video.paused);
      };
      const emitDuration = () => {
        if (Number.isFinite(video.duration) && video.duration > 0) {
          this.durationSecChange.emit(video.duration);
        }
      };

      const applyPendingSeek = () => {
        if (this.pendingSeekSec == null) {
          return;
        }
        const duration = Number.isFinite(video.duration) ? video.duration : null;
        const clamped =
          duration && duration > 0
            ? Math.max(0, Math.min(this.pendingSeekSec, duration))
            : Math.max(0, this.pendingSeekSec);
        video.currentTime = clamped;
        this.pendingSeekSec = null;
        this.currentTimeSecChange.emit(video.currentTime || 0);
      };

      if (Hls.isSupported()) {
        hls = new Hls({
          enableWorker: true,
          lowLatencyMode: false,
        });
        hls.loadSource(url);
        hls.attachMedia(video);
      } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
        video.src = url;
      }
      video.addEventListener('timeupdate', emitCurrentTime);
      video.addEventListener('seeking', emitCurrentTime);
      video.addEventListener('loadedmetadata', emitDuration);
      video.addEventListener('durationchange', emitDuration);
      video.addEventListener('play', emitPlaying);
      video.addEventListener('pause', emitPlaying);
      video.addEventListener('loadedmetadata', applyPendingSeek);
      video.addEventListener('canplay', applyPendingSeek);
      video.addEventListener('seeked', emitCurrentTime);

      onCleanup(() => {
        video.removeEventListener('timeupdate', emitCurrentTime);
        video.removeEventListener('seeking', emitCurrentTime);
        video.removeEventListener('loadedmetadata', emitDuration);
        video.removeEventListener('durationchange', emitDuration);
        video.removeEventListener('play', emitPlaying);
        video.removeEventListener('pause', emitPlaying);
        video.removeEventListener('loadedmetadata', applyPendingSeek);
        video.removeEventListener('canplay', applyPendingSeek);
        video.removeEventListener('seeked', emitCurrentTime);
        hls?.destroy();
        video.removeAttribute('src');
        video.load();
        this.pendingSeekSec = null;
      });
    });

    effect(() => {
      const seekTo = this.seekToSec();
      const ref = this.videoRef();
      if (seekTo == null || !ref) {
        return;
      }
      const video = ref.nativeElement;
      this.pendingSeekSec = seekTo;
      if (video.readyState < 1) {
        return;
      }
      const duration = Number.isFinite(video.duration) ? video.duration : null;
      const clamped = duration && duration > 0 ? Math.max(0, Math.min(seekTo, duration)) : Math.max(0, seekTo);
      if (Math.abs(video.currentTime - clamped) > 0.01) {
        video.currentTime = clamped;
        this.currentTimeSecChange.emit(video.currentTime || 0);
      }
      this.pendingSeekSec = null;
    });

    effect(() => {
      const shouldPlay = this.isPlaying();
      const ref = this.videoRef();
      if (shouldPlay == null || !ref) {
        return;
      }
      const video = ref.nativeElement;
      if (shouldPlay) {
        void video.play().catch(() => {
          this.playingChange.emit(false);
        });
      } else {
        video.pause();
      }
    });
  }
}
