class Playlist {
  private readonly queueContainer: HTMLElement;
  private readonly queueTab: HTMLElement;
  private readonly overlay: HTMLElement;
  private readonly queueList: HTMLOListElement;
  private readonly clearButton: HTMLAnchorElement;

  private readonly queueInitialHeight: string;
  private readonly queueExpandedHeight = 'calc(100% - 6ex)';

  // event handlers
  private changedHandlers: Array<() => void> = [];
  private playerDelegate: () => Player;

  // the actual episode queue
  private readonly episodes: Array<[Episode, HTMLLIElement]> = [];
  private readonly episodeHash: Set<string> = new Set<string>();

  constructor(playerDelegate: () => Player) {
    this.playerDelegate = playerDelegate;
    this.queueContainer = getOrThrow(
      document.getElementById('queue-container')
    );
    this.queueInitialHeight = getComputedStyle(this.queueContainer).height;
    this.queueTab = getOrThrow(
      this.queueContainer.getElementsByTagName('h2').item(0)
    );
    this.queueList = getOrThrow(
      this.queueContainer.getElementsByTagName('ol').item(0)
    );
    this.overlay = getOrThrow(document.getElementById('overlay'));
    this.clearButton = getOrThrow(
      <HTMLAnchorElement>document.getElementById('clear-playlist')
    );

    // wire up global playlist controls
    this.queueTab.addEventListener('click', () => this.toggleQueueUI());
    this.overlay.addEventListener('click', () => this.toggleQueueUI());
    this.clearButton.addEventListener('click', () => this.clearPlaylist());
  }

  public initialize(): void {
    this.playerDelegate().addStateChangedHandler(() => this.stateChanged());
    this.unshiftCurrent();
  }

  public addPlaylistChangedHandler(handler: () => void): void {
    this.changedHandlers.push(handler);
  }

  public hasNextEpisode(): boolean {
    return this.episodes.length > 0;
  }

  public nextEpisode(): Episode | null {
    if (this.episodes.length > 0) {
      return this.episodes[0][0];
    }
    return null;
  }

  public pushEpisode(episode: Episode): void {
    this.pushEpisodes([episode]);
  }

  public pushEpisodes(episodes: Array<Episode>): void {
    for (const episode of episodes) {
      if (this.isQueued(episode)) this.removeEpisode(episode, true, false);
      const litem = this.createQueueListItem(episode);
      this.episodes.push([episode, litem]);
      this.episodeHash.add(episode.id);
      this.queueList.appendChild(litem);
    }
    this.notify(
      episodes.length == 1
        ? 'Episode added to queue.'
        : `${episodes.length} episodes added to queue.`
    );
    this.playlistChanged();
  }

  public unshiftEpisodes(episodes: Array<Episode>): void {
    this.shiftCurrent();
    const nowPlaying = this.playerDelegate().currentEpisode();
    if (nowPlaying) {
      const litem = this.createQueueListItem(nowPlaying);
      this.episodes.unshift([nowPlaying, litem]);
      this.episodeHash.add(nowPlaying.id);
      this.queueList.prepend(litem);
    }
    for (let i = episodes.length - 1; i >= 0; i--) {
      const episode = episodes[i];
      if (this.isQueued(episode)) this.removeEpisode(episode, true, false);
      const litem = this.createQueueListItem(episode);
      this.episodes.unshift([episode, litem]);
      this.episodeHash.add(episode.id);
      this.queueList.prepend(litem);
    }
    this.notify(
      episodes.length == 1
        ? 'Episode added to queue.'
        : `${episodes.length} episodes added to queue.`
    );
    this.unshiftCurrent();
    this.playlistChanged();
  }

  public removeEpisode(
    episode: Episode,
    ignoreChanged = false,
    notify = true
  ): void {
    if (this.isQueued(episode)) {
      const idx = this.episodes.findIndex((e) => e[0].id == episode.id);
      const deleted = this.episodes.splice(idx, 1);
      this.episodeHash.delete(episode.id);
      deleted[0][1].remove();
    }
    if (!ignoreChanged) this.playlistChanged();
    if (notify) this.notify('Episode removed from queue.');
  }

  public isQueued(episode: Episode): boolean {
    return this.episodeHash.has(episode.id);
  }

  public triggerPlaylistChanged(): void {
    this.playlistChanged();
  }

  public clearPlaylist(): void {
    let removed = this.episodes.length;
    if (this.playerDelegate().currentEpisode() != null) {
      this.playerDelegate().stopPlaybackAndResetUi();
      removed++;
    }

    this.episodes.length = 0;
    this.episodeHash.clear();
    this.queueList.innerHTML = '';
    this.unshiftCurrent();
    this.playlistChanged();
    if (removed > 0) {
      this.notify('Playlist cleared.');
    }
  }

  public reQueueNowPlaying(): void {
    const currentEpisode = this.playerDelegate().currentEpisode();
    if (currentEpisode) {
      this.unshiftEpisodes([currentEpisode]);
    }
  }

  private playlistChanged(): void {
    this.shiftCurrent();
    this.unshiftCurrent();
    for (const handler of this.changedHandlers) {
      handler();
    }
  }

  private shiftCurrent(): void {
    this.queueList.firstChild?.remove();
  }

  private stateChanged(): void {
    this.shiftCurrent();
    this.unshiftCurrent();
  }

  private unshiftCurrent(): void {
    const currentEpisode = this.playerDelegate().currentEpisode();
    if (currentEpisode) {
      this.queueList.prepend(this.createQueueListItem(currentEpisode, false));
    } else {
      const litem = document.createElement('li');
      litem.classList.add('episode');
      litem.title = 'No episode playing';
      const label = document.createElement('label');
      label.innerHTML = 'No episode playing';
      litem.appendChild(label);
      this.queueList.prepend(litem);
    }
  }

  private toggleQueueUI(): void {
    if (this.queueContainer.style.height !== this.queueExpandedHeight) {
      this.queueContainer.style.height = this.queueExpandedHeight;
      this.overlay.style.backgroundColor = 'rgba(255, 255, 255, 0.75)';
      this.overlay.style.backdropFilter = 'blur(5px)';
      this.overlay.style.pointerEvents = 'auto';
      this.queueTab.innerHTML = 'Close';
      this.queueTab.classList.add('expanded');
    } else {
      this.queueContainer.style.height = this.queueInitialHeight;
      this.overlay.style.backgroundColor = 'rgba(255, 255, 255, 0)';
      this.overlay.style.backdropFilter = 'none';
      this.overlay.style.pointerEvents = 'none';
      this.queueTab.innerHTML = 'Playlist';
      this.queueTab.classList.remove('expanded');
    }
  }

  private createQueueListItem(
    episode: Episode,
    queueControls = true
  ): HTMLLIElement {
    const item = document.createElement('li');
    item.classList.add('episode');
    item.title = episode.title;
    const label = document.createElement('label');
    label.innerHTML = episode.title;
    const sspan = document.createElement('span');
    sspan.innerHTML = episode.series.title;
    const aside = document.createElement('aside');
    aside.appendChild(sspan);
    item.appendChild(label);
    item.appendChild(aside);
    if (queueControls) {
      // play now/remove buttons
      const controls = document.createElement('div');
      controls.classList.add('controls');
      const playBtn = document.createElement('a');
      playBtn.href = '#';
      playBtn.innerHTML = 'Play Now';
      playBtn.addEventListener('click', (e) => {
        this.reQueueNowPlaying();
        this.playerDelegate().playEpisode(episode);
        e.preventDefault();
      });
      const remBtn = document.createElement('a');
      remBtn.href = '#';
      remBtn.innerHTML = 'Remove';
      remBtn.addEventListener('click', (e) => {
        this.removeEpisode(episode);
        e.preventDefault();
      });
      controls.appendChild(playBtn);
      controls.appendChild(remBtn);
      item.appendChild(controls);
    } else {
      // pause/skip buttons
      item.classList.add('playing');
      const controls = document.createElement('div');
      controls.classList.add('controls');
      const pauseBtn = document.createElement('a');
      pauseBtn.href = '#';
      pauseBtn.innerHTML = this.playerDelegate().isPlaying() ? 'Pause' : 'Play';
      if (this.playerDelegate().isPlaying()) {
        pauseBtn.classList.add('pause');
      }
      pauseBtn.addEventListener('click', () => {
        this.playerDelegate().playPause();
        pauseBtn.innerHTML = this.playerDelegate().isPlaying()
          ? 'Pause'
          : 'Play';
      });
      const skipBtn = document.createElement('a');
      skipBtn.href = '#';
      skipBtn.innerHTML = 'Skip';
      skipBtn.addEventListener('click', (e) => {
        this.playerDelegate().nextEpisode();
        e.preventDefault();
      });
      controls.appendChild(pauseBtn);
      controls.appendChild(skipBtn);
      item.appendChild(controls);
    }
    return item;
  }

  private notify(message: string): void {
    Toastify({
      text: message,
      gravity: 'bottom',
      position: 'right',
      stopOnFocus: false,
      style: {
        marginBottom: '10ex',
        background: '#090',
        color: '#fff',
      },
    }).showToast();
  }
}
