<template>
  <div
    id="stream-display"
    ref="streamDisplay"
    class="stream-display uk-width-1-1 uk-height-1-1 scrollTarget"
  >
    <img
      v-if="thisStreamOpen"
      ref="click-frame"
      class="uk-align-center uk-margin-remove-bottom"
      :hidden="!streamEnabled"
      :src="streamImgUri"
      alt="Stream"
      @dblclick="clickMonitor"
    />

    <div v-if="!streamEnabled" class="uk-height-1-1">
      <div v-if="$store.state.waiting" class="uk-position-center">
        <div uk-spinner="ratio: 4.5"></div>
      </div>

      <div
        v-else-if="$store.state.globalSettings.disableStream"
        class="uk-position-center position-relative text-center"
      >
        Stream preview disabled
      </div>

      <div v-else class="uk-position-center position-relative text-center">
        No active connection
      </div>
    </div>
  </div>
</template>

<script>
import axios from "axios";

// Export main app
export default {
  name: "StreamDisplay",

  data: function() {
    return {
      displaySize: [0, 0],
      displayPosition: [0, 0],
      fov: [0, 0],
      resizeTimeoutId: setTimeout(this.doneResizing, 500)
    };
  },

  computed: {
    streamEnabled: function() {
      return (
        this.$store.getters.ready &&
        !this.$store.state.globalSettings.disableStream
      );
    },
    thisStreamOpen: function() {
      // Only a single MJPEG connection should be open at a time
      return !(this.displaySize[0] == 0) && !(this.displaySize[1] == 0);
    },
    streamImgUri: function() {
      return `${this.$store.getters.baseUri}/api/v2/streams/mjpeg`;
    },
    startPreviewUri: function() {
      return `${this.$store.getters.baseUri}/api/v2/actions/camera/preview/start`;
    },
    stopPreviewUri: function() {
      return `${this.$store.getters.baseUri}/api/v2/actions/camera/preview/stop`;
    },
    settingsUri: function() {
      return `${this.$store.getters.baseUri}/api/v2/instrument/settings`;
    }
  },

  mounted() {
    console.log(`${this._uid} mounted`);
    // A global signal listener to change the GPU preview state
    this.$root.$on("globalTogglePreview", state => {
      this.previewRequest(state);
    });
    this.$root.$on("globalSafeTogglePreview", state => {
      this.safePreviewRequest(state);
    });
    // A global signal listener to flash the stream element
    this.$root.$on("globalFlashStream", () => {
      this.flashStream();
    });

    // Mutation observer
    this.sizeObserver = new ResizeObserver(() => {
      this.handleResize(); // For any element attached to the observer, run handleResize() on change
    });
    // Fetch streamDisplay component by ref
    const streamDisplayElement = this.$refs.streamDisplay.parentNode;
    // Attach streamDisplay to the size observer
    this.sizeObserver.observe(streamDisplayElement);
  },

  created: function() {
    console.log(`${this._uid} created`);
    // Send a request to start/stop GPU preview based on global setting
    this.safePreviewRequest(this.$store.state.globalSettings.autoGpuPreview);
    // Get FOV from settings
    this.updateFov();
  },

  beforeDestroy: function() {
    console.log(`${this._uid} being destroyed`);
    // Remove global signal listener to change the GPU preview state
    this.$root.$off("globalTogglePreview");
    // Remove global signal listener to flash the stream element
    this.$root.$off("globalFlashStream");
    // Disconnect the size observer
    this.sizeObserver.disconnect();
    // Remove from the array of active streams
    this.$store.commit("removeStream", this._uid);
  },

  methods: {
    flashStream: function() {
      // Run an animation that flashes the stream (for capture feedback)
      let element = this.$refs.streamDisplay;
      element.classList.remove("uk-animation-fade");
      element.offsetHeight; /* trigger reflow */
      element.classList.add("uk-animation-fade");
      setTimeout(function() {
        element.classList.remove("uk-animation-fade");
      }, 800);
    },

    clickMonitor: function(event) {
      // Calculate steps from event coordinates and store config FOV
      let xCoordinate = event.offsetX;
      let yCoordinate = event.offsetY;

      // Simply scaling by naturalHeight/offsetHeight may give the wrong answer!
      // because we use content-fit: contain in the stylesheet, the img element
      // may be larger than the picture.  So, we must determine whether the
      // width or height is setting the scaling factor, and use a uniform scale
      // factor.
      let scale = Math.max(
        event.target.naturalWidth / event.target.offsetWidth,
        event.target.naturalHeight / event.target.offsetHeight
      );

      let xRelative = (0.5 * event.target.offsetWidth - xCoordinate) * scale;
      let yRelative = (0.5 * event.target.offsetHeight - yCoordinate) * scale;

      // Emit a signal to move, acted on by panelNavigate.vue
      this.$root.$emit(
        "globalMoveInImageCoordinatesEvent",
        -xRelative,
        -yRelative
      );
    },

    handleResize: function() {
      // Only fires resize event after no resize in 500ms (prevents resize event spam)
      clearTimeout(this.resizeTimeoutId);
      this.resizeTimeoutId = setTimeout(this.handleDoneResize, 250);
    },

    handleDoneResize: function() {
      // Recalculate size
      console.log(`Recalculating frame size for ${this._uid}`);
      this.recalculateSize();
      // Handle closed stream
      if (this.displaySize[0] == 0 && this.displaySize[1] == 0) {
        console.log(`${this._uid} is zero`);
        // If this stream was previously active
        if (this.$store.state.activeStreams[this._uid] == true) {
          console.log(`${this._uid} was active`);
          console.log(`STREAM ${this._uid} CLOSED`);
          this.$store.commit("removeStream", this._uid);
          // If all streams are closed, request the GPU preview close
          var a = Object.values(this.$store.state.activeStreams);
          let allClosed = a.every(v => v === false);
          if (allClosed) {
            this.safePreviewRequest(false);
          }
        }
        // If resized to anything other than zero
      } else {
        console.log(`STREAM ${this._uid} OPEN`);
        this.$store.commit("addStream", this._uid);
        if (this.$store.state.globalSettings.autoGpuPreview == true) {
          // Start the preview immediately
          this.safePreviewRequest(true);
          // Send another start preview request after a timeout
          /*
          The internal logic of this component means that a stop GPU preview
          request will only ever be sent if ALL stream components are invisible.
          However, when switching tabs, sometimes the previous component will
          react to becoming invisible before the new tab has become visible.
          This results in a stop-preview request being sent JUST before the new
          start preview request is sent. In an ideal network, this is fine, however
          due to network jitter, these requests will occasionally be recieved out
          of order, causing the preview to start in the new location, and then 
          quickly stop again.
          Preventing this properly will involve some clever server-side logic
          probably, however as a pit-stop solution, we always send another
          start-preview request after 1 second. This means that either:
          1. The initial requests happened in order, in which case this follow-up
          request does nothing, or
          2. The requests leapfrog eachother, stopping the preview, in which case
          the follow-up request restarts the preview in the correct location.
          */
          setTimeout(() => this.safePreviewRequest(true), 250);
        }
      }
    },

    recalculateSize: function() {
      // Calculate stream size
      let element = this.$refs.streamDisplay.parentNode;
      let bound = element.getBoundingClientRect();

      let elementSize = [bound.width, bound.height];

      let elementPositionOnWindow = [bound.left, bound.top];
      let windowPositionOnDisplay = [window.screenX, window.screenY];
      let windowChromeHeight = window.outerHeight - window.innerHeight;
      let elementPositionOnDisplay = [
        Math.max(0, windowPositionOnDisplay[0] + elementPositionOnWindow[0]),
        Math.max(
          0,
          windowPositionOnDisplay[1] +
            elementPositionOnWindow[1] +
            windowChromeHeight
        )
      ];

      this.displaySize = elementSize;
      this.displayPosition = elementPositionOnDisplay;
    },

    safePreviewRequest: function(state) {
      // previewRequest, but only stopping preview if all streams are invisible
      // and only starting preview if any stream is visible
      var a = Object.values(this.$store.state.activeStreams);
      let allClosed = a.every(v => v === false);
      // If all streams are closed, don't start GPU preview
      if (state === true && allClosed) {
        return false;
      }
      // If not all streams are closed, don't stop GPU preview
      if (state === false && !allClosed) {
        return false;
      }
      return this.previewRequest(state);
    },

    previewRequest: function(state) {
      // If requesting starting the stream, but this component is inactive, skip
      if (this.$store.getters.ready == true) {
        var requestUri = null;
        // Create URI
        if (state == true) {
          requestUri = this.startPreviewUri;
        } else {
          requestUri = this.stopPreviewUri;
        }

        // Generate payload if tracking window position
        var payload = {};
        if (
          this.$store.state.globalSettings.trackWindow == true &&
          state == true
        ) {
          // Recalculate frame dimensions and position
          this.recalculateSize();
          // Copy data into payload array
          payload = {
            window: [
              this.displayPosition[0],
              this.displayPosition[1],
              this.displaySize[0],
              this.displaySize[1]
            ]
          };
        }

        // Send preview request
        console.log(`${this._uid} toggled preview to ${state}`);
        axios
          .post(requestUri, payload)
          .then(() => {})
          .catch(error => {
            this.modalError(error); // Let mixin handle error
          });
      }
    },

    updateFov: function() {
      console.log("Updating FOV");
      // Get the current field-of-view setting from the server
      axios
        .get(`${this.settingsUri}/fov`)
        .then(response => {
          if (response.data) {
            this.fov = response.data;
          }
        })
        .catch(error => {
          this.modalError(error); // Let mixin handle error
        });
    }
  }
};
</script>

<style scoped lang="less">
.stream-display img {
  height: 100%;
  text-align: center;
  object-fit: contain;
}

.stream-display {
  width: 100%;
  height: 100%;
}

.position-relative {
  position: relative !important;
}

.text-center {
  text-align: center;
}
</style>
