import React, { Component } from 'react';
import injectWbResizeEvent from '/imports/ui/components/presentation/resize-wrapper/component';
import { defineMessages, injectIntl } from 'react-intl';
import ReactPlayer from 'react-player';
import {
  sendMessage,
  onMessage,
  removeAllListeners,
  getPlayingState,
} from './service';

import {
  isMobile, isTablet,
} from '../layout/utils';

import logger from '/imports/startup/client/logger';

import VolumeSlider from './volume-slider/component';
import ReloadButton from '/imports/ui/components/reload-button/component';

import ArcPlayer from '/imports/ui/components/external-video-player/custom-players/arc-player';
import PeerTubePlayer from '/imports/ui/components/external-video-player/custom-players/peertube';

import { styles } from './styles';

const intlMessages = defineMessages({
  autoPlayWarning: {
    id: 'app.externalVideo.autoPlayWarning',
    description: 'Shown when user needs to interact with player to make it work',
  },
  refreshLabel: {
    id: 'app.externalVideo.refreshLabel',
  },
});

const SYNC_INTERVAL_SECONDS = 5;
const THROTTLE_INTERVAL_SECONDS = 0.5;
const AUTO_PLAY_BLOCK_DETECTION_TIMEOUT_SECONDS = 5;

ReactPlayer.addCustomPlayer(PeerTubePlayer);
ReactPlayer.addCustomPlayer(ArcPlayer);

class VideoPlayer extends Component {
  static clearVideoListeners() {
    removeAllListeners('play');
    removeAllListeners('stop');
    removeAllListeners('playerUpdate');
    removeAllListeners('presenterReady');
  }

  constructor(props) {
    super(props);

    const { isPresenter } = props;

    this.player = null;
    this.syncInterval = null;
    this.autoPlayTimeout = null;
    this.hasPlayedBefore = false;
    this.playerIsReady = false;

    this.lastMessage = null;
    this.lastMessageTimestamp = Date.now();

    this.throttleTimeout = null;

    this.state = {
      muted: false,
      playing: false,
      autoPlayBlocked: false,
      volume: 1,
      playbackRate: 1,
      key: 0,
    };

    this.opts = {
      // default option for all players, can be overwritten
      playerOptions: {
        autoplay: true,
        playsinline: true,
        controls: isPresenter,
      },
      file: {
        attributes: {
          controls: isPresenter ? 'controls' : '',
          autoplay: 'autoplay',
          playsinline: 'playsinline',
        },
      },
      dailymotion: {
        params: {
          controls: isPresenter,
        },
      },
      youtube: {
        playerVars: {
          autoplay: 1,
          modestbranding: 1,
          autohide: 1,
          rel: 0,
          ecver: 2,
          controls: isPresenter ? 1 : 0,
        },
      },
      peertube: {
        isPresenter,
      },
      twitch: {
        options: {
          controls: isPresenter,
        },
        playerId: 'externalVideoPlayerTwitch',
      },
      preload: true,
      showHoverToolBar: false,
    };

    this.registerVideoListeners = this.registerVideoListeners.bind(this);
    this.autoPlayBlockDetected = this.autoPlayBlockDetected.bind(this);
    this.handleFirstPlay = this.handleFirstPlay.bind(this);
    this.handleReload = this.handleReload.bind(this);
    this.handleOnProgress = this.handleOnProgress.bind(this);
    this.handleOnReady = this.handleOnReady.bind(this);
    this.handleOnPlay = this.handleOnPlay.bind(this);
    this.handleOnPause = this.handleOnPause.bind(this);
    this.handleVolumeChanged = this.handleVolumeChanged.bind(this);
    this.handleOnMuted = this.handleOnMuted.bind(this);
    this.sendSyncMessage = this.sendSyncMessage.bind(this);
    this.getCurrentPlaybackRate = this.getCurrentPlaybackRate.bind(this);
    this.getCurrentTime = this.getCurrentTime.bind(this);
    this.getCurrentVolume = this.getCurrentVolume.bind(this);
    this.getMuted = this.getMuted.bind(this);
    this.setPlaybackRate = this.setPlaybackRate.bind(this);
    this.onBeforeUnload = this.onBeforeUnload.bind(this);

    this.isMobile = isMobile() || isTablet();
    this.mobileHoverSetTimeout = null;
  }

  componentDidMount() {
    const {
      getSwapLayout,
      toggleSwapLayout,
      layoutContextDispatch,
    } = this.props;

    window.addEventListener('beforeunload', this.onBeforeUnload);

    clearInterval(this.syncInterval);
    clearTimeout(this.autoPlayTimeout);

    VideoPlayer.clearVideoListeners();
    this.registerVideoListeners();

    if (getSwapLayout()) toggleSwapLayout(layoutContextDispatch);
  }

  shouldComponentUpdate(nextProps, nextState) {
    const { isPresenter } = this.props;
    const { playing } = this.state;

    // If user is presenter we don't re-render playing state changes
    // Because he's in control of the play/pause status
    if (nextProps.isPresenter && isPresenter && nextState.playing !== playing) {
      return false;
    }

    return true;
  }

  componentDidUpdate(prevProp) {
    // Detect presenter change and redo the sync and listeners to reassign video to the new one
    const { isPresenter } = this.props;
    if (isPresenter !== prevProp.isPresenter) {
      VideoPlayer.clearVideoListeners();

      clearInterval(this.syncInterval);
      clearTimeout(this.autoPlayTimeout);

      this.registerVideoListeners();
    }
  }

  componentWillUnmount() {
    window.removeEventListener('beforeunload', this.onBeforeUnload);

    VideoPlayer.clearVideoListeners();

    clearInterval(this.syncInterval);
    clearTimeout(this.autoPlayTimeout);

    this.player = null;
  }

  handleOnReady() {
    const { hasPlayedBefore, playerIsReady } = this;

    if (hasPlayedBefore || playerIsReady) {
      return;
    }

    this.playerIsReady = true;

    this.autoPlayTimeout = setTimeout(
      this.autoPlayBlockDetected,
      AUTO_PLAY_BLOCK_DETECTION_TIMEOUT_SECONDS * 1000,
    );
  }

  handleFirstPlay() {
    const { isPresenter } = this.props;
    const { hasPlayedBefore } = this;

    if (!hasPlayedBefore) {
      this.hasPlayedBefore = true;
      this.setState({ autoPlayBlocked: false });

      clearTimeout(this.autoPlayTimeout);

      if (isPresenter) {
        this.sendSyncMessage('presenterReady');
      }
    }
  }

  handleOnPlay() {
    const { isPresenter } = this.props;
    const { playing } = this.state;

    const curTime = this.getCurrentTime();

    if (isPresenter && !playing) {
      this.sendSyncMessage('play', { time: curTime });
    }
    this.setState({ playing: true });

    this.handleFirstPlay();

    if (!isPresenter && !playing) {
      this.setState({ playing: false });
    }
  }

  handleOnPause() {
    const { isPresenter } = this.props;
    const { playing } = this.state;

    const curTime = this.getCurrentTime();

    if (isPresenter && playing) {
      this.sendSyncMessage('stop', { time: curTime });
    }
    this.setState({ playing: false });

    this.handleFirstPlay();

    if (!isPresenter && playing) {
      this.setState({ playing: true });
    }
  }

  handleOnProgress() {
    const volume = this.getCurrentVolume();
    const muted = this.getMuted();

    this.setState({ volume, muted });
  }

  handleVolumeChanged(volume) {
    this.setState({ volume });
  }

  handleOnMuted(muted) {
    this.setState({ muted });
  }

  handleReload() {
    const { key } = this.state;
    // increment key and force a re-render of the video component
    this.setState({ key: key + 1 });
  }

  onBeforeUnload() {
    const { isPresenter } = this.props;

    if (isPresenter) {
      this.sendSyncMessage('stop');
    }
  }

  static getDerivedStateFromProps(props) {
    const { inEchoTest } = props;

    return { mutedByEchoTest: inEchoTest };
  }

  getCurrentTime() {
    if (this.player && this.player.getCurrentTime) {
      return Math.round(this.player.getCurrentTime());
    }
    return 0;
  }

  getCurrentPlaybackRate() {
    const intPlayer = this.player && this.player.getInternalPlayer();

    return (intPlayer && intPlayer.getPlaybackRate && intPlayer.getPlaybackRate()) || 1;
  }

  setPlaybackRate(rate) {
    const intPlayer = this.player && this.player.getInternalPlayer();
    const currentRate = this.getCurrentPlaybackRate();

    if (currentRate === rate) {
      return;
    }

    this.setState({ playbackRate: rate });
    if (intPlayer && intPlayer.setPlaybackRate) {
      intPlayer.setPlaybackRate(rate);
    }
  }

  getCurrentVolume() {
    const { volume } = this.state;
    const intPlayer = this.player && this.player.getInternalPlayer();

    return (intPlayer && intPlayer.getVolume && intPlayer.getVolume() / 100.0) || volume;
  }

  getMuted() {
    const { muted } = this.state;
    const intPlayer = this.player && this.player.getInternalPlayer();

    return (intPlayer && intPlayer.isMuted && intPlayer.isMuted()) || muted;
  }

  autoPlayBlockDetected() {
    this.setState({ autoPlayBlocked: true });
  }

  sendSyncMessage(msg, params) {
    const timestamp = Date.now();

    // If message is just a quick pause/un-pause just send nothing
    const sinceLastMessage = (timestamp - this.lastMessageTimestamp) / 1000;
    if ((
      (msg === 'play' && this.lastMessage === 'stop')
      || (msg === 'stop' && this.lastMessage === 'play'))
         && sinceLastMessage < THROTTLE_INTERVAL_SECONDS) {
      return clearTimeout(this.throttleTimeout);
    }

    // Ignore repeat presenter ready messages
    if (this.lastMessage === msg && msg === 'presenterReady') {
      logger.debug('Ignoring a repeated presenterReady message');
    } else {
      // Play/pause messages are sent with a delay, to permit cancelling it in case of
      // quick sucessive play/pauses
      const messageDelay = (msg === 'play' || msg === 'stop') ? THROTTLE_INTERVAL_SECONDS : 0;

      this.throttleTimeout = setTimeout(() => {
        sendMessage(msg, { ...params });
      }, messageDelay * 1000);

      this.lastMessage = msg;
      this.lastMessageTimestamp = timestamp;
    }
    return true;
  }

  registerVideoListeners() {
    const { isPresenter } = this.props;

    if (isPresenter) {
      this.syncInterval = setInterval(() => {
        const { playing } = this.state;
        const curTime = this.getCurrentTime();
        const rate = this.getCurrentPlaybackRate();

        // Always pause video if presenter is has not started sharing, e.g., blocked by autoplay
        const playingState = this.hasPlayedBefore ? playing : false;

        this.sendSyncMessage('playerUpdate', { rate, time: curTime, state: playingState });
      }, SYNC_INTERVAL_SECONDS * 1000);
    } else {
      onMessage('play', ({ time }) => {
        const { hasPlayedBefore, player } = this;

        if (!player || !hasPlayedBefore) {
          return;
        }
        this.seekTo(time);
        this.setState({ playing: true });

        logger.debug({ logCode: 'external_video_client_play' }, 'Play external video');
      });

      onMessage('stop', ({ time }) => {
        const { hasPlayedBefore, player } = this;

        if (!player || !hasPlayedBefore) {
          return;
        }
        this.seekTo(time);
        this.setState({ playing: false });

        logger.debug({ logCode: 'external_video_client_stop' }, 'Stop external video');
      });

      onMessage('presenterReady', () => {
        const { hasPlayedBefore } = this;

        logger.debug({ logCode: 'external_video_presenter_ready' }, 'Presenter is ready to sync');

        if (!hasPlayedBefore) {
          this.setState({ playing: true });
        }
      });

      onMessage('playerUpdate', (data) => {
        const { hasPlayedBefore, player } = this;
        const { playing } = this.state;
        const { time, rate, state } = data;

        if (!player || !hasPlayedBefore) {
          return;
        }

        if (rate !== this.getCurrentPlaybackRate()) {
          this.setPlaybackRate(rate);
          logger.debug({
            logCode: 'external_video_client_update_rate',
            extraInfo: {
              newRate: rate,
            },
          }, 'Change external video playback rate.');
        }

        this.seekTo(time);

        const playingState = getPlayingState(state);
        if (playing !== playingState) {
          this.setState({ playing: playingState });
        }
      });
    }
  }

  seekTo(time) {
    const { player } = this;

    if (!player) {
      return logger.error('No player on seek');
    }

    // Seek if viewer has drifted too far away from presenter
    if (Math.abs(this.getCurrentTime() - time) > SYNC_INTERVAL_SECONDS * 0.75) {
      player.seekTo(time, true);
      logger.debug({
        logCode: 'external_video_client_update_seek',
        extraInfo: { time },
      }, `Seek external video to: ${time}`);
    }
    return true;
  }

  render() {
    const {
      videoUrl,
      isPresenter,
      intl,
      top,
      left,
      right,
      height,
      width,
    } = this.props;

    const {
      playing, playbackRate, mutedByEchoTest, autoPlayBlocked,
      volume, muted, key, showHoverToolBar,
    } = this.state;

    const mobileHoverToolBarStyle = showHoverToolBar
      ? styles.showMobileHoverToolbar
      : styles.dontShowMobileHoverToolbar;
    const desktopHoverToolBarStyle = styles.hoverToolbar;

    const hoverToolbarStyle = this.isMobile ? mobileHoverToolBarStyle : desktopHoverToolBarStyle;
    return (
      <span
        style={{
          position: 'absolute',
          top,
          left,
          right,
          height,
          width,
        }}
      >
        <div
          id="video-player"
          data-test="videoPlayer"
          className={styles.videoPlayerWrapper}
          ref={(ref) => { this.playerParent = ref; }}
        >
          {
            autoPlayBlocked
              ? (
                <p className={styles.autoPlayWarning}>
                  {intl.formatMessage(intlMessages.autoPlayWarning)}
                </p>
              )
              : ''
          }

          <ReactPlayer
            className={styles.videoPlayer}
            url={videoUrl}
            config={this.opts}
            volume={(muted || mutedByEchoTest) ? 0 : volume}
            muted={muted || mutedByEchoTest}
            playing={playing}
            playbackRate={playbackRate}
            onProgress={this.handleOnProgress}
            onReady={this.handleOnReady}
            onPlay={this.handleOnPlay}
            onPause={this.handleOnPause}
            key={`react-player${key}`}
            ref={(ref) => { this.player = ref; }}
            height="100%"
            width="100%"
          />
          {
            !isPresenter
              ? [
                (
                  <div className={hoverToolbarStyle} key="hover-toolbar-external-video">
                    <VolumeSlider
                      volume={volume}
                      muted={muted || mutedByEchoTest}
                      onMuted={this.handleOnMuted}
                      onVolumeChanged={this.handleVolumeChanged}
                    />

                    <ReloadButton
                      handleReload={this.handleReload}
                      label={intl.formatMessage(intlMessages.refreshLabel)}
                    />
                  </div>
                ),
                (this.isMobile && playing) && (
                  <span
                    className={styles.mobileControlsOverlay}
                    key="mobile-overlay-external-video"
                    ref={(ref) => { this.overlay = ref; }}
                    onTouchStart={() => {
                      clearTimeout(this.mobileHoverSetTimeout);
                      this.setState({ showHoverToolBar: true });
                    }}
                    onTouchEnd={() => {
                      this.mobileHoverSetTimeout = setTimeout(
                        () => this.setState({ showHoverToolBar: false }),
                        5000,
                      );
                    }}
                  />
                ),
              ]
              : null
          }
        </div>
      </span>
    );
  }
}

export default injectIntl(injectWbResizeEvent(VideoPlayer));
