// ==========================================================================
// Plyr UI
// ==========================================================================

import captions from './captions';
import controls from './controls';
import support from './support';
import { getElement, toggleClass } from './utils/elements';
import { ready, triggerEvent } from './utils/events';
import i18n from './utils/i18n';
import is from './utils/is';
import loadImage from './utils/load-image';

const ui = {
  addStyleHook() {
    toggleClass(this.elements.container, this.config.selectors.container.replace('.', ''), true);
    toggleClass(this.elements.container, this.config.classNames.uiSupported, this.supported.ui);
  },

  // Toggle native HTML5 media controls
  toggleNativeControls(toggle = false) {
    if (toggle && this.isHTML5) {
      this.media.setAttribute('controls', '');
    } else {
      this.media.removeAttribute('controls');
    }
  },

  // Setup the UI
  build() {
    // Re-attach media element listeners
    // TODO: Use event bubbling?
    this.listeners.media();

    // Don't setup interface if no support
    if (!this.supported.ui) {
      this.debug.warn(`Basic support only for ${this.provider} ${this.type}`);

      // Restore native controls
      ui.toggleNativeControls.call(this, true);

      // Bail
      return;
    }

    // Inject custom controls if not present
    if (!is.element(this.elements.controls)) {
      // Inject custom controls
      controls.inject.call(this);

      // Re-attach control listeners
      this.listeners.controls();
    }

    // Remove native controls
    ui.toggleNativeControls.call(this);

    // Setup captions for HTML5
    if (this.isHTML5) {
      captions.setup.call(this);
    }

    // Reset volume
    this.volume = null;

    // Reset mute state
    this.muted = null;

    // Reset loop state
    this.loop = null;

    // Reset quality setting
    this.quality = null;

    // Reset speed
    this.speed = null;

    // Reset volume display
    controls.updateVolume.call(this);

    // Reset time display
    controls.timeUpdate.call(this);

    // Reset duration display
    controls.durationUpdate.call(this);

    // Update the UI
    ui.checkPlaying.call(this);

    // Check for picture-in-picture support
    toggleClass(
      this.elements.container,
      this.config.classNames.pip.supported,
      support.pip && this.isHTML5 && this.isVideo,
    );

    // Check for airplay support
    toggleClass(this.elements.container, this.config.classNames.airplay.supported, support.airplay && this.isHTML5);

    // Add touch class
    toggleClass(this.elements.container, this.config.classNames.isTouch, this.touch);

    // Ready for API calls
    this.ready = true;

    // Ready event at end of execution stack
    setTimeout(() => {
      triggerEvent.call(this, this.media, 'ready');
    }, 0);

    // Set the title
    ui.setTitle.call(this);

    // Assure the poster image is set, if the property was added before the element was created
    if (this.poster) {
      ui.setPoster.call(this, this.poster, false).catch(() => {});
    }

    // Manually set the duration if user has overridden it.
    // The event listeners for it doesn't get called if preload is disabled (#701)
    if (this.config.duration) {
      controls.durationUpdate.call(this);
    }

    // Media metadata
    if (this.config.mediaMetadata) {
      controls.setMediaMetadata.call(this);
    }
  },

  // Setup aria attribute for play and iframe title
  setTitle() {
    // Find the current text
    let label = i18n.get('play', this.config);

    // If there's a media title set, use that for the label
    if (is.string(this.config.title) && !is.empty(this.config.title)) {
      label += `, ${this.config.title}`;
    }

    // If there's a play button, set label
    Array.from(this.elements.buttons.play || []).forEach((button) => {
      button.setAttribute('aria-label', label);
    });

    // Set iframe title
    // https://github.com/sampotts/plyr/issues/124
    if (this.isEmbed) {
      const iframe = getElement.call(this, 'iframe');

      if (!is.element(iframe)) {
        return;
      }

      // Default to media type
      const title = !is.empty(this.config.title) ? this.config.title : 'video';
      const format = i18n.get('frameTitle', this.config);

      iframe.setAttribute('title', format.replace('{title}', title));
    }
  },

  // Toggle poster
  togglePoster(enable) {
    toggleClass(this.elements.container, this.config.classNames.posterEnabled, enable);
  },

  // Set the poster image (async)
  // Used internally for the poster setter, with the passive option forced to false
  setPoster(poster, passive = true) {
    // Don't override if call is passive
    if (passive && this.poster) {
      return Promise.reject(new Error('Poster already set'));
    }

    // Set property synchronously to respect the call order
    this.media.setAttribute('data-poster', poster);

    // Show the poster
    this.elements.poster.removeAttribute('hidden');

    // Wait until ui is ready
    return (
      ready
        .call(this)
        // Load image
        .then(() => loadImage(poster))
        .catch((error) => {
          // Hide poster on error unless it's been set by another call
          if (poster === this.poster) {
            ui.togglePoster.call(this, false);
          }
          // Rethrow
          throw error;
        })
        .then(() => {
          // Prevent race conditions
          if (poster !== this.poster) {
            throw new Error('setPoster cancelled by later call to setPoster');
          }
        })
        .then(() => {
          Object.assign(this.elements.poster.style, {
            backgroundImage: `url('${poster}')`,
            // Reset backgroundSize as well (since it can be set to "cover" for padded thumbnails for youtube)
            backgroundSize: '',
          });

          ui.togglePoster.call(this, true);

          return poster;
        })
    );
  },

  // Check playing state
  checkPlaying(event) {
    // Class hooks
    toggleClass(this.elements.container, this.config.classNames.playing, this.playing);
    toggleClass(this.elements.container, this.config.classNames.paused, this.paused);
    toggleClass(this.elements.container, this.config.classNames.stopped, this.stopped);

    // Set state
    Array.from(this.elements.buttons.play || []).forEach((target) => {
      Object.assign(target, { pressed: this.playing });
      target.setAttribute('aria-label', i18n.get(this.playing ? 'pause' : 'play', this.config));
    });

    // Only update controls on non timeupdate events
    if (is.event(event) && event.type === 'timeupdate') {
      return;
    }

    // Toggle controls
    ui.toggleControls.call(this);
  },

  // Check if media is loading
  checkLoading(event) {
    this.loading = ['stalled', 'waiting'].includes(event.type);

    // Clear timer
    clearTimeout(this.timers.loading);

    // Timer to prevent flicker when seeking
    this.timers.loading = setTimeout(
      () => {
        // Update progress bar loading class state
        toggleClass(this.elements.container, this.config.classNames.loading, this.loading);

        // Update controls visibility
        ui.toggleControls.call(this);
      },
      this.loading ? 250 : 0,
    );
  },

  // Toggle controls based on state and `force` argument
  toggleControls(force) {
    const { controls: controlsElement } = this.elements;

    if (controlsElement && this.config.hideControls) {
      // Don't hide controls if a touch-device user recently seeked. (Must be limited to touch devices, or it occasionally prevents desktop controls from hiding.)
      const recentTouchSeek = this.touch && this.lastSeekTime + 2000 > Date.now();

      // Show controls if force, loading, paused, button interaction, or recent seek, otherwise hide
      this.toggleControls(
        Boolean(
          force || this.loading || this.paused || controlsElement.pressed || controlsElement.hover || recentTouchSeek,
        ),
      );
    }
  },

  // Migrate any custom properties from the media to the parent
  migrateStyles() {
    // Loop through values (as they are the keys when the object is spread 🤔)
    Object.values({ ...this.media.style })
      // We're only fussed about Plyr specific properties
      .filter((key) => !is.empty(key) && is.string(key) && key.startsWith('--plyr'))
      .forEach((key) => {
        // Set on the container
        this.elements.container.style.setProperty(key, this.media.style.getPropertyValue(key));

        // Clean up from media element
        this.media.style.removeProperty(key);
      });

    // Remove attribute if empty
    if (is.empty(this.media.style)) {
      this.media.removeAttribute('style');
    }
  },
};

export default ui;
