import utils from "../plugins/utils";

/**
 * This plugin (available in all vue components as $audioPlayer) handles
 * access to the shared audio player, and events related to loading and
 * playback of audio attachments.
 * 
 * Components use this by calling "addListener" (and corresponding removeListener) with
 * an audio matrix event and a unique component id (for example the ._uid property).
 */
export default {
  install(Vue) {
    class SharedAudioPlayer {
      constructor() {
        this.player = new Audio();
        this.currentEvent = null;
        this.currentClapReactions = [];
        this.infoMap = new Map();
        this.player.addEventListener("durationchange", this.onDurationChange.bind(this));
        this.player.addEventListener("timeupdate", this.onTimeUpdate.bind(this));
        this.player.addEventListener("play", this.onPlay.bind(this));
        this.player.addEventListener("pause", this.onPause.bind(this));
        this.player.addEventListener("ended", this.onEnded.bind(this));
      }

      getPlayerElement() {
        return this.player;
      }

      addListener(uid, event) {
        const eventId = event.getId();
        var entry = this.infoMap.get(eventId);
        if (!entry) {
            // Listeners is just a Set of component "uid" entries for now.
          entry = { url: null, listeners: new Set() };
          // Make these reactive, so AudioPlayer (and others) can listen to them
          Vue.set(entry, "loading", false);
          Vue.set(entry, "loadPercent", 0);
          Vue.set(entry, "duration", 0);
          Vue.set(entry, "currentTime", 0);
          Vue.set(entry, "playPercent", 0);
          Vue.set(entry, "playing", false);
          this.infoMap.set(eventId, entry);

          // Get duration information
          utils
            .getAttachmentUrlAndDuration(event)
            .then(([ignoredurl, duration]) => {
              entry.duration = duration;
            })
            .catch((err) => {
              console.error("Failed to fetch attachment duration: ", err);
            });
        }
        entry.listeners.add(uid);
        return entry;
      }
      removeListener(uid) {
        [...this.infoMap].forEach(([ignoredeventid, info]) => {
          info.listeners.delete(uid);
          if (info.listeners.size == 0 && info.url) {
            // No more listeners, release audio blob
            URL.revokeObjectURL(info.url);
            info.url = null;
          }
        });
        this.infoMap = new Map(
          [...this.infoMap].filter(([ignoredeventid, info]) => {
            return info.listeners.size > 0;
          })
        );
      }

      play(event, timelineSet) {
        this.play_(event, timelineSet, false);
      }
      
      load(event, timelineSet) {
        this.play_(event, timelineSet, true);
      }

      play_(event, timelineSet, onlyLoad) {
        const eventId = event.getId();
        if (this.currentEvent != eventId) {
          // Media change, pause the one currently playing.
          this.player.pause();
          var entry = this.infoMap.get(this.currentEvent);
          if (entry) {
            entry.playing = false;
          }
        }
        this.currentEvent = eventId;
        const info = this.infoMap.get(eventId);
        if (info) {

          // Get all clap reactions
          this.initializeClapEvents(event, timelineSet);

          if (info.url) {
            // Restart from beginning?
            if (info.currentTime == info.duration) {
              info.currentTime = 0;
              info.playPercent = 0;
            }
            if (this.player.src != info.url) {
              this.player.src = info.url;
              this.player.currentTime = (info.currentTime || 0) / 1000;
            }
            if (onlyLoad) {
              this.player.load();
            } else {
              this.player.play();
            }
          } else {
            // Download it!
            info.loadPercent = 0;
            info.loading = true;
            info.abortController = new AbortController();
            utils
              .getAttachment(this.$root.$matrix.matrixClient, event, (progress) => {
                info.loadPercent = progress;
              }, false, info.abortController)
              .then((url) => {
                info.url = url;

                // Still on this item? Call ourselves recursively.
                if (this.currentEvent == eventId) {
                  if (onlyLoad) {
                    this.load(event, timelineSet);
                  } else {
                    this.play(event, timelineSet);
                  }
                }
              })
              .catch((err) => {
                console.error("Failed to fetch attachment: ", err);
              })
              .finally(() => {
                info.loading = false;
                info.abortController = undefined;
              });
          }
        }
      }

      /**
       * Set the "autoplay" property on the underlying player object.
       * @param {} autoplay 
       */
      setAutoplay(autoplay) {
        this.player.autoplay = autoplay;
      }

      pause(event) {
        if (!event || this.currentEvent == event.getId()) {
          this.player.pause();
        }

        if (event) {
          // If downloading, abort that!
          var entry = this.infoMap.get(event.getId());
          if (entry && entry.abortController) {
            entry.abortController.abort();
          }
        }
      }
      seek(event, percent) {
        var entry = this.infoMap.get(event.getId());
        if (entry) {
          entry.currentTime = ((percent / 100) * (entry.duration || 0));
          this.updatePlayPercent(entry);
          if (this.currentEvent == event.getId()) {
            this.player.currentTime = entry.currentTime / 1000;
          }
        }
      }
      seekRelative(event, milliseconds) {
        var entry = this.infoMap.get(event.getId());
        if (entry) {
          entry.currentTime = Math.max(0, Math.min(entry.currentTime + milliseconds, entry.duration));
          this.updatePlayPercent(entry);
          if (this.currentEvent == event.getId()) {
            this.player.currentTime = entry.currentTime / 1000;
          }
        }
      }
      onPlay() {
        var entry = this.infoMap.get(this.currentEvent);
        if (entry) {
          entry.playing = true;
        }
        this.$root.$emit("audio-playback-started", this.currentEvent);
      }
      onPause() {
        var entry = this.infoMap.get(this.currentEvent);
        if (entry) {
          entry.playing = false;
        }
        this.$root.$emit("audio-playback-paused", this.currentEvent);
      }
      onEnded() {
        var entry = this.infoMap.get(this.currentEvent);
        if (entry) {
          entry.playing = false;
          entry.currentTime = entry.duration; // Next time restart
        }
        this.$root.$emit("audio-playback-ended", this.currentEvent);
      }
      onTimeUpdate() {
        var entry = this.infoMap.get(this.currentEvent);
        if (entry) {
          const oldTime = entry.currentTime;
          entry.currentTime = 1000 * this.player.currentTime;
          this.updatePlayPercent(entry);
          this.maybePlayClapEvent(oldTime, entry.currentTime);
        }
      }
      onDurationChange() {
        const duration =
          this.player.duration && isFinite(this.player.duration) && !isNaN(this.player.duration)
            ? 1000 * this.player.duration
            : 0;
        var entry = this.infoMap.get(this.currentEvent);
        if (entry) {
          entry.duration = duration;
          this.updatePlayPercent(entry);
        }
      }
      updatePlayPercent(entry) {
        if (entry.duration > 0) {
          entry.playPercent = Math.floor((100 / entry.duration) * entry.currentTime);
        } else {
          entry.playPercent = 0;
        }
      }

      initializeClapEvents(event, timelineSet) {
        if (event) {
          const reactions = timelineSet.relations.getChildEventsForEvent(event.getId(), 'm.annotation', 'm.reaction');
          if (reactions) {
          this.currentClapReactions = reactions.getRelations()
            .filter(r => r.getRelation().key == "👏" && r.getRelation().timeOffset && parseInt(r.getRelation().timeOffset) > 0)
            .map(r => { 
              return {
                sender: r.getSender(),
                emoji: r.getRelation().key,
                timeOffset: parseInt(r.getRelation().timeOffset)
              }
            })
            .sort((a,b) => a.timeOffset - b.timeOffset);
          }
        } else {
          this.currentClapReactions = [];
        }
      }

      maybePlayClapEvent(previousTimeMs, timeNowMs) {
        (this.currentClapReactions || []).forEach(reaction => {
          if (previousTimeMs < reaction.timeOffset && timeNowMs >= reaction.timeOffset) {
            this.$root.$emit("audio-playback-reaction", reaction);
          }
        });
      }
    }

    Vue.prototype.$audioPlayer = new SharedAudioPlayer();
  },
};
