<template>
  <div class="annotated-image" ref="root">
    <template v-if="src">
      <options-bar :file="current"
                   :interaction="interaction"
                   @interaction="interaction = $event"
                   :filter="filter"
                   @filter="filter = $event"
                   :labelsEnabled="labelsEnabled"
                   @labelSelector="openLabelSelector()"
                   :zoomBox="zoomBox"
                   :infoBox="infoBox !== false"
                   @infoBox="infoBox = $event; interaction=false"
                   @unzoom="zoomBox=false; interaction=false"
                   @predict="predictImage"
                   @confirmAll="confirmAll"
                   @prevzoom="$refs.overlay.prevZoom()"
                   @nextzoom="$refs.overlay.nextZoom()"/>

      <label-selector v-if="labelSelector"
                      :labels="labels"
                      @close="closeLabelSelector()"
                      @label="labelSelected($event);"
                      />

      <div class="media">
        <h3>{{current.path}}</h3>
        <div class="mode-tooltip">{{modeTooltip}}</div>

        <!-- image -->
        <img v-if="current.type === 'image'"
             ref="media" :src="src" alt="media"
             :style="cropPosition"
             @load="change"
             @loadedmetadata="change"
             @loadeddata="change"
             @transitionend="resize">

        <!-- video -->
        <template v-if="current.type === 'video'">
          <div class="video-player">
            <video ref="media" :src="src"
                   v-on:load="change" v-on:loadedmetadata="change" v-on:loadeddata="change" v-on:canplay="change"
                   v-on:timeupdate="videoProgress"/>
          </div>
          <video-control :file="current"
                         :video="video"
                         :results="results"
                         @play="videoPlay"
                         @jump="videoJump"/>
        </template>

        <!-- overlay -->
        <annotation-overlay ref="overlay"
                            :file="current"
                            :position="overlayPosition"
                            :size="image"
                            :interaction="interaction"
                            @interaction="interaction = $event"
                            :filter="filter"
                            :label="label"
                            :video="video"
                            :results="results"
                            :labels="labels"
                            :crop="infoBox"
                            @labelSelector="openLabelSelector($event)"
                            @labelBox="labelBox"
                            @newBox="newBox"
                            @labelImage="labelImage"
                            @remove="remove"
                            @confirm="confirm"
                            @updateBox="updateBox"
                            @estimateBox="estimateBox"
                            @crop="infoBox = $event"
                            :zoom="zoomBox"
                            @zoom="zoomBox = $event"/>
      </div>

      <cropped-image v-if="infoBox !== false"
                     :labels="labels"
                     :file="current"
                     :box="infoBox"
                     @predictBox="predictBox"
                     @removeLabel="labelBox($event, null)"
                     @setLabel="openLabelSelector($event)"
                     @close="
                      interaction=defaultInteraction;
                      infoBox=false"
                    />
    </template>
  </div>
</template>

<script>
import AnnotationOverlay from "@/components/media/annotation-overlay";
import VideoControl from "@/components/media/video-control";
import OptionsBar from "@/components/media/options-bar";
import CroppedImage from "@/components/media/cropped-image";
import LabelSelector from "@/components/media/label-selector";

export default {
  name: "annotated-image",
  components: {
    OptionsBar,
    VideoControl,
    AnnotationOverlay,
    CroppedImage,
    LabelSelector
  },
  props: [
    'current'
  ],

  data: function () {

    return {
      interval: false,
      container: {
        top: 0,
        left: 0,
        width: 0,
        height: 0,
      },
      image: {
        top: 0,
        left: 0,
        width: 0,
        height: 0
      },
      video: {
        frame: 0,
        play: false
      },
      infoBox: false,
      zoomBox: false,
      supported: {
        labeledImages: false,
        boundingBoxes: false,
        labeledBoundingBoxes: false,
      },
      interaction: 'draw-box',
      defaultInteraction: 'draw-box',
      filter: ['user', 'pipeline'],
      label: false,
      labelSelector: false,
      boxTolabel: null,
      model: null,
      results: [],
      labels: []
    }
  },
  mounted: function () {
    // add resize listener
    window.addEventListener('resize', this.resize);
    this.interval = setInterval(this.resize, 1000);
    this.resize();

    // add result listener
    this.$root.socket.on('create-result', this.createResult);
    this.$root.socket.on('edit-result', this.editResult);
    this.$root.socket.on('remove-result', this.removeResult);

    // add label listener
    this.getLabels();
    this.$root.socket.on('connect', this.getLabels);
    this.$root.socket.on('create-label', this.addLabelToList);
    this.$root.socket.on('remove-label', this.removeLabelFromList);
    this.$root.socket.on('edit-label', this.editLabelInList);

    // get model
    this.$root.socket.get(`/projects/${this.$root.project.identifier}/model`)
      .then(response => response.json())
      .then(model => {
        this.model = model;
        this.supported.labeledImages = model.supports.includes('labeled-images');
        this.supported.labeledBoundingBoxes = model.supports.includes('labeled-bounding-boxes');
        this.supported.boundingBoxes = this.supported.labeledBoundingBoxes
          || model.supports.includes('bounding-boxes');
      });
  },
  destroyed: function () {
    window.removeEventListener('resize', this.resize);
    clearInterval(this.interval);

    this.$root.socket.off('create-result', this.createResult);
    this.$root.socket.off('edit-result', this.editResult);
    this.$root.socket.off('remove-result', this.removeResult);

    this.$root.socket.off('connect', this.getLabels);
    this.$root.socket.off('create-label', this.addLabelToList);
    this.$root.socket.off('remove-label', this.removeLabelFromList);
    this.$root.socket.off('edit-label', this.editLabelInList);
  },
  computed: {
    src: function () {
      if (!this.container.width || !this.container.height)
        return '';

      if (this.current.type === 'video')
        return this.$root.socket.media(this.current);

      const width = Math.ceil(this.container.width / 400) * 400;
      const height = Math.ceil(this.container.height / 400) * 400;
      return this.$root.socket.media(this.current, width, height);
    },
    overlayPosition: function () {
      return {
        top: (this.image.top - this.container.top) + 'px',
        left: (this.image.left - this.container.left) + 'px',
        width: this.image.width + 'px',
        height: this.image.height + 'px'
      }
    },
    cropPosition: function () {
      if (!this.zoomBox)
        return {
          transform: ``,
        };

      const posX = 0.5 - (this.zoomBox.x + this.zoomBox.w / 2);
      const posY = 0.5 - (this.zoomBox.y + this.zoomBox.h / 2);
      const factor = 0.75 / Math.max(this.zoomBox.w, this.zoomBox.h);

      // use a transition to use the transitionend event to recalculate box positions
      return {
        transform: `scale(${factor}) translateX(${posX * 100}%) translateY(${posY * 100}%)`
      };
    },
    labelsEnabled: function() {

      return this.model
          && (this.supported.labeledImages || this.supported.labeledBoundingBoxes);
    },

    modeTooltip: function(){
      let tip = "Current mode";
      switch (this.interaction) {
        case "draw-box":
          return `${tip}: draw bounding-box`;

        case "estimate-box":
          return `${tip}: estimate bounding-box`;

        case "extreme-clicking":
          return `${tip}: extreme clicking`;

        case "move-box":
          return `${tip}: move or resize`;

        case "label-box":
          if (this.label.identifier === null)
            return `${tip}: remove tag`;
          else
            return `${tip}: tag as "${this.label.name}"`;

        case "confirm-box":
          return `${tip}: confirm`;

        case "remove-box":
          return `${tip}: remove`;

        case "info-box":
          return `${tip}: bounding-box info`;

        default:
          return "";
      }
    },
  },
  methods: {

    openLabelSelector: function(box) {
      if (box === null || box === undefined)
        this.boxTolabel = null
      else
        this.boxTolabel = box;

      this.labelSelector = true;
    },

    closeLabelSelector: function() {
      this.boxTolabel = null;
      this.labelSelector = false;
    },

    labelSelected: function(label) {
      this.interaction = 'label-box';
      this.label = label;

      if (this.boxTolabel !== null)
        this.labelBox(this.boxTolabel.identifier, label.identifier);
    },

    labelBox: function(box_id, label_id) {
      this.$root.socket.post(`/results/${box_id}/label`,
        {label: label_id});
    },

    newBox: function(file_id, data) {
      this.$root.socket.post(`/data/${file_id}/results`,
        {type: 'bounding-box', data: data});
    },

    labelImage: function(file_id, label_id) {
      this.$root.socket.post(`/data/${file_id}/results`,
        {type: 'labeled-image', label: label_id});
    },

    updateBox: function(box_id, data) {
      this.$root.socket.post(`/results/${box_id}/data`,
        {data: data});
    },

    estimateBox: function(file_id, coordinates) {
      this.$root.socket.post(`/data/${file_id}/estimate`,
        coordinates);
    },

    predictImage: function(file_id) {
      this.$root.socket.post(`/data/${file_id}/predict`,
        {predict: true});
    },

    predictBox: function(file_id, box_id) {
      this.$root.socket.post(`/data/${file_id}/${box_id}/predict_bounding_box`,
        {predict: true});
    },

    // confirm all bboxes of the image
    confirmAll: function(file_id) {
      this.$root.socket.post(`/data/${file_id}/confirm_all`,
        {confirm: true});
    },

    // confirm either a bbox or image result
    confirm: function(res_id) {
      this.$root.socket.post(`/results/${res_id}/confirm`,
        {confirm: true});
    },
    // remove either a bbox or image result
    remove: function(res_id,) {
      this.$root.socket.post(`/results/${res_id}/remove`,
        {remove: true});
    },

    resize: function () {
      const rect = this.$refs.root.getBoundingClientRect();

      this.container.top = rect.top;
      this.container.left = rect.left;
      this.container.width = rect.width;
      this.container.height = rect.height;

      this.change();
    },
    change: function () {
      if (!this.$refs.media)
        return;

      const rect = this.$refs.media.getBoundingClientRect();

      this.image.top = rect.top;
      this.image.left = rect.left;
      this.image.width = rect.width;
      this.image.height = rect.height;
    },
    videoProgress: function (event) {
      this.video.frame = Math.floor(event.target.currentTime * this.current.fps);
    },
    videoPlay: function (value) {
      this.video.play = value;
      if (value)
        this.$refs.media.play();
      else
        this.$refs.media.pause();
    },
    videoJump: function (value) {
      // calculate difference
      value = Math.max(0, Math.min(this.current.frames, value));
      const diff = value - this.video.frame;

      // set timestamp
      this.$refs.media.currentTime += diff / this.current.fps;

      // get timestamp offset and correct if needed
      const currentFrame = this.$refs.media.currentTime * this.current.fps;

      if (diff < 0 && currentFrame < value) {
        this.$refs.media.currentTime += 0.5 / this.current.fps;
      } else if (diff > 0 && currentFrame > value + 0.5) {
        this.$refs.media.currentTime -= 0.5 / this.current.fps;
      }
    },
    createResult: function (result) {
      if (result['file_id'] !== this.current.identifier)
        return;

      for (let r of this.results)
        if (r.identifier === result.identifier)
          return;

      this.results.push(result);
    },
    removeResult: function (result) {
      for (let i = 0; i < this.results.length; i++) {
        if (this.results[i].identifier === result.identifier) {
          this.results.splice(i, 1);
          return;
        }
      }
    },
    editResult: function (result) {
      if (this.infoBox && result.identifier === this.infoBox.identifier)
        this.infoBox = result;

      for (let i = 0; i < this.results.length; i++) {
        if (this.results[i].identifier === result.identifier) {
          this.$set(this.results, i, result);
          return;
        }
      }
    },
    getLabels: function () {
      this.$root.socket.get(`/projects/${this.$root.project.identifier}/labels`)
        .then(response => response.json())
        .then(labels => {
          this.labels = [];
          labels.forEach(this.addLabelToList);
        });
    },
    addLabelToList: function (label) {
      if (label['project_id'] !== this.$root.project.identifier)
        return;

      for (let l of this.labels)
        if (l.identifier === label.identifier)
          return;

      this.labels.push(label);
    },
    removeLabelFromList: function (label) {
      for (let i = 0; i < this.labels.length; i++) {
        if (this.labels[i].identifier === label.identifier) {
          this.labels.splice(i, 1);
          return;
        }
      }
    },
    editLabelInList: function (label) {
      for (let i = 0; i < this.labels.length; i++) {
        if (this.labels[i].identifier === label.identifier) {
          this.$set(this.labels, i, label);
          return;
        }
      }
    }
  },
  watch: {
    current: {
      immediate: true,
      handler: function (newVal) {
        this.video.play = false;
        this.video.frame = 0;

        this.zoomBox = false;

        this.$root.socket.get(`/data/${newVal.identifier}/results`)
          .then(response => response.json())
          .then(results => {
            this.results = results;
          });
      }
    },
    infoBox: function () {
      setTimeout(this.resize, 1);
    }
  }
}
</script>

<style scoped>
.annotated-image {
  width: 100%;
  height: 100%;

  display: flex;
  flex-direction: row;
  justify-content: center;
  align-items: center;

  overflow: hidden;

}

.options-bar {
  height: 100%;
}

.media {
  width: 100%;
  height: 100%;
  flex-grow: 1;

  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;

  overflow: hidden;
}

.video-player {
  width: 100%;
  height: 100%;
  flex-grow: 1;
  overflow-y: auto;

  display: flex;
  justify-content: center;
  align-items: center;
}

img, video {
  max-width: 100%;
  max-height: 90%;
  transition: transform 0.01s;
}

h3 {
  max-width: 100%;
  max-height: 5%;
  margin: 0.5em;
}

.mode-tooltip {
  max-width: 100%;
  max-height: 5%;
  margin: 0.5em;
}


</style>
