<template>
  <div class="page-container md-layout-column" id="dashboard-container">

    <!-- toolbar -->
    <toolbar @showDrawerClicked="showDrawer = true"
             @applyClicked="performApply"
             :processing="anyPendingNodeMoves || processingNodeChanges || processingModelDownload"
             :model-changed="modelChanged"
    />

    <!-- drawer -->
    <md-drawer :md-active.sync="showDrawer" md-swipeable>
      <workspace-drawer-content :pending-changes="pendingChanges"/>
    </md-drawer>

    <!-- workspace -->
    <md-content id="diagram-container">
      <simple-flowchart
          :scene.sync="model"
          @nodeDelete="enqueuePendingNodeDeletion"
          @nodeDataEdited="handleNodeDataEdit"
          @linkBreak="handleLinkBreak"
          @linkAdded="handleLinkAdd"
          @nodeMoved="handleNodeMove"
      />
      <element-adder @addElement="newElement"/>
    </md-content>

  </div>
</template>

<script>

import SimpleFlowchart from '@/simple-flowchart';
import ElementAdder from "@/components/ElementAdder";
import WorkspaceDrawerContent from "@/components/WorkspaceDrawerContent";
import Toolbar from "@/components/Toolbar";

import {some, map, cloneDeep} from 'lodash';

export default {
  name: 'Dashboard',
  components: {
    SimpleFlowchart,
    ElementAdder,
    WorkspaceDrawerContent,
    Toolbar
  },
  data: () => ({
    showDrawer: false,
    idGenerator: 1,
    pendingChanges: {
      created: [],
      updated: [],
      deleted: [] // Stores apiId instead of localId
    },
    pendingNodeMoves: 0, // count of inflight coord modifications
    processingNodeChanges: false,
    processingModelDownload: false,
    model: {
      centerX: 0,
      centerY: 0,
      scale: 1,
      nodes: [],
      links: []
    }
  }),
  methods: {
    newElement(type) {
      this.showNewElementChooser = false;

      let newNode = {
        x: 10,
        y: 10,
        type,
        apiId: null
      }

      switch (type) {
        case "ingest":
          newNode.data = {
            url: "",
            streamkey: ""
          }
          break;
        case "encoder":
          newNode.data = {
            bitrate: 2500,
            width: 1280,
            height: 720
          }
          break;
        case "restreamer":
          newNode.data = {
            url: "",
            streamkey: ""
          }
          break;
      }

      newNode.id = this.idGenerator;
      this.idGenerator++;

      this.model.nodes.push(newNode);
      //this.pendingChanges.created.add(newNode.id);
      this.enqueuePendingNodeCreation(newNode.id);

    },
    // Vuejs can not track changes of a set, so we do this magic hack to fix it
    enqueuePendingNodeDeletion({id, apiId}) {
      let pending_deletes = new Set(this.pendingChanges.deleted);
      let pending_updates = new Set(this.pendingChanges.updated);
      let pending_creations = new Set(this.pendingChanges.created);

      // This stuff is here to prevent the creation and deletion of an object in the same transaction
      if (pending_creations.has(id)) {
        pending_creations.delete(id);
      } else {
        if (apiId) {
          pending_deletes.add(apiId);
        }
      }

      pending_updates.delete(id);

      this.pendingChanges.deleted = Array.from(pending_deletes);
      this.pendingChanges.updated = Array.from(pending_updates);
      this.pendingChanges.created = Array.from(pending_creations);
    },
    enqueuePendingNodeUpdate(id) {
      let pending_updates = new Set(this.pendingChanges.updated);
      pending_updates.add(id);
      this.pendingChanges.updated = Array.from(pending_updates);
    },
    enqueuePendingNodeCreation(id) {
      let pending_creations = new Set(this.pendingChanges.created);
      pending_creations.add(id);
      this.pendingChanges.created = Array.from(pending_creations);
    },
    // --- end of magic hack
    handleNodeDataEdit(id) {
      if (!this.pendingChanges.created.includes(id)) {
        this.enqueuePendingNodeUpdate(id);
      }
    },
    handleNodeMove(id) {
      const moved_node = this.model.nodes.find((node) => node.id === id);

      if ((!moved_node) || (!moved_node.apiId)) {
        return;
      }

      this.pendingNodeMoves++;
      this.$api.put(`objects/streamerobjects/coordmodify/${moved_node.apiId}`, {
        x: moved_node.x,
        y: moved_node.y
      }).then(() => {
        this.pendingNodeMoves--;
      }).catch(() => {
        this.pendingNodeMoves--;
      });

    },
    handleLinkBreak({from, to}) {
      this.enqueuePendingNodeUpdate(from);
      this.enqueuePendingNodeUpdate(to);
    },
    handleLinkAdd({from, to}) {
      this.enqueuePendingNodeUpdate(from);
      this.enqueuePendingNodeUpdate(to);
    },
    performApply() {
      this.processingNodeChanges = true;

      const pendingChangesSnapshot = cloneDeep(this.pendingChanges);
      const modelSnapshot = cloneDeep(this.model);
      this.pendingChanges = {
        created: [],
        updated: [],
        deleted: []
      }

      let creation_promises = []

      // Perform creations
      pendingChangesSnapshot.created.forEach((id) => {
        const new_node = modelSnapshot.nodes.find((n) => n.id === id);
        if (new_node) {
          creation_promises.push(new Promise((resolve, reject) => {

            let data = {
              x: new_node.x,
              y: new_node.y
            }

            let translated_type = new_node.type;

            switch (new_node.type) {
              case "ingest":
                data["outputNeighbours"] = [];
                break;
              case "restreamer": {
                let fixed_thing = new_node.data.url.slice(-1) !== '/' ? new_node.data.url + '/' : new_node.data.url;
                let full_url = new_node.data.url ? new URL("./" + new_node.data.streamkey, fixed_thing).href : null;
                data["inputNeighbour"] = new_node ? new_node.apiId : null;
                data["outputURLs"] = full_url ? [full_url] : full_url;
                translated_type = "restream";
                break;
              }
              case "encoder":
                data["inputNeighbour"] = null;
                data["outputNeighbours"] = []
                data["bitrate"] = new_node.data.bitrate;
                data["width"] = new_node.data.width;
                data["height"] = new_node.data.height;
                translated_type = "encode";
                break;
            }


            this.$api.post(`objects/streamerobjects/${translated_type}`, data).then((resp) => {
              new_node.apiId = resp.data.id;

              const original_node = this.model.nodes.find((n) => n.id === id);
              if (original_node) {
                original_node.apiId = resp.data.id;
              }

              if (resp.data.resource_type === 1) {
                new_node.data.url = resp.data.url;
                original_node.data.url = resp.data.url;

                new_node.data.streamkey = resp.data.stream_key;
                original_node.data.streamkey = resp.data.stream_key;
              }

              return resolve();
            }).catch(reject);

          }));

        }

      });

      // Perform Updates
      Promise.all(creation_promises).then(() => {

        let update_promises = [];
        pendingChangesSnapshot.updated.forEach((id) => {
          const changed_node = modelSnapshot.nodes.find((n) => n.id === id);
          if (changed_node) {
            update_promises.push(new Promise((resolve, reject) => {

              const api_id = changed_node.apiId;
              let data = {
                x: changed_node.x,
                y: changed_node.y
              }
              // Find neighbors


              let input_neighbor = null;
              let output_neighbors = [];
              if (modelSnapshot.links.length > 0) {
                // Input first
                const input_link = modelSnapshot.links.find((link) => link.to === changed_node.id);
                if (input_link) {
                  input_neighbor = modelSnapshot.nodes.find((node) => node.id === input_link.from);
                }

                // Then output
                const output_links = modelSnapshot.links.filter((link) => link.from === changed_node.id);
                if (output_links.length > 0) {
                  output_neighbors = modelSnapshot.nodes.filter((node) => map(output_links, 'to').includes(node.id));
                }
              }

              // Compile type specific attributes
              let translated_type = changed_node.type;
              switch (changed_node.type) {
                case "ingest":
                  data["outputNeighbours"] = map(output_neighbors, 'apiId');
                  break;
                case "restreamer": {
                  let fixed_thing = changed_node.data.url.slice(-1) !== '/' ? changed_node.data.url + '/' : changed_node.data.url;
                  let full_url = changed_node.data.url ? new URL("./" + changed_node.data.streamkey, fixed_thing).href : null;
                  data["inputNeighbour"] = input_neighbor ? input_neighbor.apiId : null;
                  data["outputURLs"] = full_url ? [full_url] : full_url;
                  translated_type = "restream";
                  break;
                }
                case "encoder":
                  data["inputNeighbour"] = input_neighbor ? input_neighbor.apiId : null;
                  data["outputNeighbours"] = map(output_neighbors, 'apiId');
                  data["bitrate"] = changed_node.data.bitrate;
                  data["width"] = changed_node.data.width;
                  data["height"] = changed_node.data.height;
                  translated_type = "encode";
                  break;
              }

              // and then send it
              this.$api.put(`objects/streamerobjects/${translated_type}/${api_id}`, data).then(() => {
                return resolve();
              }).catch(reject);

            }));
          }
        });

        // Perform deletions
        Promise.all(update_promises).then(() => {

          let delete_promises = [];
          pendingChangesSnapshot.deleted.forEach((apiId) => {
            delete_promises.push(
                this.$api.delete(`objects/streamerobjects/${apiId}`)
            );
          });

          Promise.all(delete_promises).then(() => {
            this.processingNodeChanges = false;
          });

        });

      });

    },
  },
  mounted() {
    this.processingModelDownload = true;
    this.$api.get('objects/streamerobjects').then((resp) => {

      // Load bare nodes first
      resp.data.forEach((apiNode) => {

        const type_map = {
          1: "ingest",
          2: "encoder",
          3: "restreamer"
        }

        let newNode = {
          x: apiNode.x,
          y: apiNode.y,
          type: type_map[apiNode.resource_type],
          apiId: apiNode.id
        }

        switch (newNode.type) {
          case "ingest":
            newNode.data = {
              url: apiNode.url,
              streamkey: apiNode.stream_key
            }
            break;
          case "encoder":
            newNode.data = {
              bitrate: apiNode.bitrate,
              width: apiNode.width,
              height: apiNode.height
            }
            break;
          case "restreamer": {
            let full_url = apiNode.output_urls[0] || '';
            let url = '';
            let key = '';
            if (full_url) {
              url = full_url.substring(0, full_url.lastIndexOf("/") + 1);
              key = full_url.substring(full_url.lastIndexOf("/") + 1, full_url.length);
            }
            newNode.data = {
              url: url,
              streamkey: key
            }
            break;
          }
        }

        newNode.id = this.idGenerator;
        this.idGenerator++;

        this.model.nodes.push(newNode);

      });

      // Load links between nodes
      let linkIdGenerator = 1;
      resp.data.forEach((apiNode) => {
        if (apiNode.inputNeighbour) {
          const dst_node = this.model.nodes.find((node) => node.apiId === apiNode.id);
          const src_node = this.model.nodes.find((node) => node.apiId === apiNode.inputNeighbour);
          if (src_node && dst_node) {

            const newLink = {
              id: linkIdGenerator,
              from: src_node.id,
              to: dst_node.id
            }

            linkIdGenerator++;

            this.model.links.push(newLink);

          }
        }
      });

      this.processingModelDownload = false;
    });
  },
  computed: {
    modelChanged() {
      return some(this.pendingChanges, (o) => o.length > 0);
    },
    anyPendingNodeMoves() {
      return this.pendingNodeMoves > 0;
    }
  }

}
</script>

<style scoped>
#dashboard-container {
  height: 100%;
}

#diagram-container {
  height: calc(100% - 64px);
}
</style>