<template>
  <div id="connections-wrapper">
    <div class="highlight-container highlight-container--bottom-labels" @click="open" @touchend="open">
      <entity-item
          v-for="(chunk, i) in chunks"
          :key="i"
          :spanid="chunk.id"
          :content="chunk.text"
          :newline="chunk.newline"
          :label="chunk.label"
          :color="chunk.color"
          :labels="labels"
          :link-types="linkTypes"
          :source-chunk="sourceChunk"
          :source-link-type="sourceLinkType"
          @remove="deleteAnnotation(chunk.id)"
          @update="updateEntity($event.id, chunk.id)"
          @selectSource="selectSource(chunk)"
          @selectTarget="selectTarget(chunk)"
          @deleteLink="deleteLink($event.id, $event.ndx)"
          @selectNewLinkType="selectNewLinkType($event)"
          @hideAllLinkMenus="hideAllLinkMenus()"
      />
      <v-menu
          v-model="showMenu"
          :position-x="x"
          :position-y="y"
          absolute
          offset-y
      >
        <v-list
            dense
            min-width="150"
            max-height="400"
            class="overflow-y-auto"
        >
          <v-list-item
              v-for="(label, i) in labels"
              :key="i"
              v-shortkey="[label.suffixKey]"
              @shortkey="assignLabel(label.id)"
              @click="assignLabel(label.id)"
          >
            <v-list-item-content>
              <v-list-item-title v-text="label.text"/>
            </v-list-item-content>
            <v-list-item-action>
              <v-list-item-action-text v-text="label.suffixKey"/>
            </v-list-item-action>
          </v-list-item>
        </v-list>
      </v-menu>
    </div>

    <canvas id="connections">
    </canvas>
  </div>
</template>

<script>
import EntityItem from './EntityItem'

export default {
  components: {
    EntityItem
  },

  props: {
    text: {
      type: String,
      default: '',
      required: true
    },
    labels: {
      type: Array,
      default: () => ([]),
      required: true
    },
    linkTypes: {
      type: Array,
      default: () => ([]),
      required: true
    },
    entities: {
      type: Array,
      default: () => ([]),
      required: true
    },
    deleteAnnotation: {
      type: Function,
      default: () => ([]),
      required: true
    },
    updateEntity: {
      type: Function,
      default: () => ([]),
      required: true
    },
    addEntity: {
      type: Function,
      default: () => ([]),
      required: true
    },
    sourceChunk: {
      type: Object,
      default: () => {
      },
      required: true
    },
    sourceLinkType: {
      type: Object,
      default: () => {
      },
      required: true
    },
    selectSource: {
      type: Function,
      default: () => ([]),
      required: true
    },
    selectTarget: {
      type: Function,
      default: () => ([]),
      required: true
    },
    deleteLink: {
      type: Function,
      default: () => ([]),
      required: true
    },
    selectNewLinkType: {
      type: Function,
      default: () => ([]),
      required: true
    },
    hideAllLinkMenus: {
      type: Function,
      default: () => ([]),
      required: true
    }
  },

  data() {
    return {
      showMenu: false,
      x: 0,
      y: 0,
      start: 0,
      end: 0
    }
  },

  computed: {
    sortedEntities() {
      return this.entities.slice().sort((a, b) => a.startOffset - b.startOffset)
    },

    chunks() {
      let chunks = []
      let startOffset = 0
      // to count the number of characters correctly.
      const characters = [...this.text]
      for (const entity of this.sortedEntities) {
        // add non-entities to chunks.
        let piece = characters.slice(startOffset, entity.startOffset).join('')
        chunks = chunks.concat(this.makeChunks(piece))
        startOffset = entity.endOffset
        // add entities to chunks.
        const label = this.labelObject[entity.label]
        piece = characters.slice(entity.startOffset, entity.endOffset).join('')
        chunks.push({
          id: entity.id,
          label: label.text,
          color: label.backgroundColor,
          text: piece,
          selectedAsLinkSource: false,
          links: entity.links ? entity.links.map(link => {
            return {
              id: link.id,
              type: link.type,
              color: this.getColor(link.type),
              targetId: link.annotation_id_2,
              targetLabel: null // target label can be computed only after all chunks are made, see line 204
            }
          }) : null
        })
      }
      // add the rest of text.
      chunks = chunks.concat(this.makeChunks(characters.slice(startOffset, characters.length).join('')));

      // populate the links. Must be done after chunk creation
      chunks.forEach(chunk => {
        if (chunk.links) {
          chunk.links.forEach(link => {
            link.targetLabel = chunks.find(target => target.id === link.targetId).text;
          });
        }
      });

      return chunks;
    },

    labelObject() {
      const obj = {}
      for (const label of this.labels) {
        obj[label.id] = label
      }
      return obj
    }
  },

  updated() {
    this.$nextTick(() => {
      const parentPos = document.getElementById('connections-wrapper').getBoundingClientRect();
      const canvas = document.getElementById('connections');
      canvas.width = parentPos.width;
      canvas.height = parentPos.height;

      const ctx = canvas.getContext('2d');
      ctx.clearRect(0, 0, parentPos.width, parentPos.height);

      const topPoints = this.drawnCountPoints(this.chunks.length);
      const bottomPoints = this.drawnCountPoints(this.chunks.length);

      const chunks = this.chunks;

      chunks.forEach(function(sourceChunk, sourceNdx) {
        if (sourceChunk.links) {
          sourceChunk.links.forEach(function(link) {
            let childPos = document.getElementById('spn-' + sourceChunk.id).getBoundingClientRect();
            const y1 = childPos.y - parentPos.y;

            childPos = document.getElementById('spn-' + link.targetId).getBoundingClientRect();
            const y2 = childPos.y - parentPos.y;

            const targetNdx = chunks.findIndex(ch => ch.id === link.targetId);

            if (y1 < y2) {
              bottomPoints[sourceNdx].count++;
              topPoints[targetNdx].count++;

            } else if (y1 > y2) {
              topPoints[sourceNdx].count++;
              bottomPoints[targetNdx].count++;

            } else {
              bottomPoints[sourceNdx].count++;
              bottomPoints[targetNdx].count++;
            }
          });
        }
      });

      chunks.forEach(function(sourceChunk, sourceNdx) {
        if (sourceChunk.links) {
          sourceChunk.links.forEach(function(link) {
            const sourcePos = document.getElementById('spn-' + sourceChunk.id).getBoundingClientRect();
            let x1 = sourcePos.x - parentPos.x;
            let y1 = sourcePos.y - parentPos.y;

            const targetPos = document.getElementById('spn-' + link.targetId).getBoundingClientRect();
            let x2 = targetPos.x - parentPos.x;
            let y2 = targetPos.y - parentPos.y;

            const targetNdx = chunks.findIndex(ch => ch.id === link.targetId);

            ctx.beginPath();
            ctx.lineWidth = 3;
            ctx.strokeStyle = link.color;

            if (y1 < y2) {
              bottomPoints[sourceNdx].drawn++;
              topPoints[targetNdx].drawn++;

              x1 += bottomPoints[sourceNdx].drawn * sourcePos.width / (bottomPoints[sourceNdx].count + 1);
              y1 += sourcePos.height;

              x2 += topPoints[targetNdx].drawn * targetPos.width / (topPoints[targetNdx].count + 1);

              ctx.moveTo(x1, y1);
              ctx.lineTo(x1, y1 + 12);
              ctx.lineTo(x2, y2 - 12);
              ctx.lineTo(x2, y2);
              ctx.stroke();

              ctx.fillStyle = link.color;
              ctx.beginPath();
              ctx.moveTo(x2, y2);
              ctx.lineTo(x2 - 3, y2 - 5);
              ctx.lineTo(x2 + 3, y2 - 5);
              ctx.lineTo(x2, y2);
              ctx.closePath();
              ctx.stroke();

            } else if (y1 > y2) {
              topPoints[sourceNdx].drawn++;
              bottomPoints[targetNdx].drawn++;

              x1 += topPoints[sourceNdx].drawn * sourcePos.width / (topPoints[sourceNdx].count + 1);

              x2 += bottomPoints[targetNdx].drawn * targetPos.width / (bottomPoints[targetNdx].count + 1);
              y2 += targetPos.height;

              ctx.moveTo(x1, y1);
              ctx.lineTo(x1, y1 - 12);
              ctx.lineTo(x2, y2 + 12);
              ctx.lineTo(x2, y2);
              ctx.stroke();

              ctx.fillStyle = link.color;
              ctx.beginPath();
              ctx.moveTo(x2, y2);
              ctx.lineTo(x2 - 3, y2 + 5);
              ctx.lineTo(x2 + 3, y2 + 5);
              ctx.lineTo(x2, y2);
              ctx.closePath();
              ctx.stroke();

            } else {
              bottomPoints[sourceNdx].drawn++;
              bottomPoints[targetNdx].drawn++;

              x1 += bottomPoints[sourceNdx].drawn * sourcePos.width / (bottomPoints[sourceNdx].count + 1);
              y1 += sourcePos.height;

              x2 += bottomPoints[targetNdx].drawn * targetPos.width / (bottomPoints[targetNdx].count + 1);
              y2 += targetPos.height;

              ctx.moveTo(x1, y1);
              ctx.lineTo(x1, y1 + 12);
              ctx.lineTo(x2, y2 + 12);
              ctx.lineTo(x2, y2);
              ctx.stroke();

              ctx.fillStyle = link.color;
              ctx.beginPath();
              ctx.moveTo(x2, y2);
              ctx.lineTo(x2 - 3, y2 + 5);
              ctx.lineTo(x2 + 3, y2 + 5);
              ctx.lineTo(x2, y2);
              ctx.closePath();
              ctx.stroke();
            }
          });
        }
      });
    });
  },

  methods: {
    makeChunks(text) {
      const chunks = []
      const snippets = text.split('\n')
      for (const snippet of snippets.slice(0, -1)) {
        chunks.push({
          label: null,
          color: null,
          text: snippet + '\n',
          newline: false
        })
        chunks.push({
          label: null,
          color: null,
          text: '',
          newline: true
        })
      }
      chunks.push({
        label: null,
        color: null,
        text: snippets.slice(-1)[0],
        newline: false
      })
      return chunks
    },

    show(e) {
      e.preventDefault()
      this.showMenu = false
      this.x = e.clientX || e.changedTouches[0].clientX
      this.y = e.clientY || e.changedTouches[0].clientY
      this.$nextTick(() => {
        this.showMenu = true
      })
    },

    setSpanInfo() {
      let selection
      // Modern browsers.
      if (window.getSelection) {
        selection = window.getSelection()
      } else if (document.selection) {
        selection = document.selection
      }
      // If nothing is selected.
      if (selection.rangeCount <= 0) {
        return
      }
      const range = selection.getRangeAt(0)
      const preSelectionRange = range.cloneRange()
      preSelectionRange.selectNodeContents(this.$el)
      preSelectionRange.setEnd(range.startContainer, range.startOffset)
      this.start = [...preSelectionRange.toString()].length
      this.end = this.start + [...range.toString()].length
    },

    validateSpan() {
      if ((typeof this.start === 'undefined') || (typeof this.end === 'undefined')) {
        return false
      }
      if (this.start === this.end) {
        return false
      }
      for (const entity of this.entities) {
        if ((entity.startOffset <= this.start) && (this.start < entity.endOffset)) {
          return false
        }
        if ((entity.startOffset < this.end) && (this.end <= entity.endOffset)) {
          return false
        }
        if ((this.start < entity.startOffset) && (entity.endOffset < this.end)) {
          return false
        }
      }
      return true
    },

    open(e) {
      this.$emit('hideAllLinkMenus');

      this.setSpanInfo()
      if (this.validateSpan()) {
        this.show(e)
      }
    },

    assignLabel(labelId) {
      if (this.validateSpan()) {
        this.addEntity(this.start, this.end, labelId)
        this.showMenu = false
        this.start = 0
        this.end = 0
      }
    },

    getColor(typeId) {
      const type = this.linkTypes.find(type => type.id === typeId);
      if (type) {
        return type.color;
      }
      return "#787878";
    },

    drawnCountPoints(size) {
      const points = Array(size);

      for (let i = 0; i < points.length; i++) {
        points[i] = {
          drawn: 0,
          count: 0
        }
      }

      return points;
    }
  }
}
</script>

<style scoped>
.highlight-container.highlight-container--bottom-labels {
  align-items: flex-start;
}

.highlight-container {
  line-height: 70px !important;
  display: flex;
  flex-wrap: wrap;
  white-space: pre-wrap;
  cursor: default;
  position: relative;
  z-index: 1;
}

.highlight-container.highlight-container--bottom-labels .highlight.bottom {
  margin-top: 6px;
}

#connections-wrapper {
  position: relative;
}

#connections-wrapper canvas {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  z-index: 0;
}
</style>