text: /*
  * Copyright (C) 2019 Intel Corporation
  * SPDX-License-Identifier: MIT
  */
  
  /* global
      require:false
  */
  
  (() => {
      const serverProxy = require('./server-proxy');
      const { Task } = require('./session');
      const { ScriptingError } = ('./exceptions');
  
      class AnnotationsSaver {
          constructor(version, collection, session) {
              this.sessionType = session instanceof Task ? 'task' : 'job';
              this.id = session.id;
              this.version = version;
              this.collection = collection;
              this.initialObjects = {};
              this.hash = this._getHash();
  
              // We need use data from export instead of initialData
              // Otherwise we have differ keys order and JSON comparison code incorrect
              const exported = this.collection.export();
  
              this._resetState();
              for (const shape of exported.shapes) {
                  this.initialObjects.shapes[shape.id] = shape;
              }
  
              for (const track of exported.tracks) {
                  this.initialObjects.tracks[track.id] = track;
              }
  
              for (const tag of exported.tags) {
                  this.initialObjects.tags[tag.id] = tag;
              }
          }
  
          _resetState() {
              this.initialObjects = {
                  shapes: {},
                  tracks: {},
                  tags: {},
              };
          }
  
          _getHash() {
              const exported = this.collection.export();
              return JSON.stringify(exported);
          }
  
          async _request(data, action) {
              const result = await serverProxy.annotations.updateAnnotations(
                  this.sessionType,
                  this.id,
                  data,
                  action,
              );
  
              return result;
          }
  
          async _put(data) {
              const result = await this._request(data, 'put');
              return result;
          }
  
          async _create(created) {
              const result = await this._request(created, 'create');
              return result;
          }
  
          async _update(updated) {
              const result = await this._request(updated, 'update');
              return result;
          }
  
          async _delete(deleted) {
              const result = await this._request(deleted, 'delete');
              return result;
          }
  
          _split(exported) {
              const splitted = {
                  created: {
                      shapes: [],
                      tracks: [],
                      tags: [],
                  },
                  updated: {
                      shapes: [],
                      tracks: [],
                      tags: [],
                  },
                  deleted: {
                      shapes: [],
                      tracks: [],
                      tags: [],
                  },
              };
  
              // Find created and updated objects
              for (const type of Object.keys(exported)) {
                  for (const object of exported[type]) {
                      if (object.id in this.initialObjects[type]) {
                          const exportedHash = JSON.stringify(object);
                          const initialHash = JSON.stringify(this.initialObjects[type][object.id]);
                          if (exportedHash !== initialHash) {
                              splitted.updated[type].push(object);
                          }
                      } else if (typeof (object.id) === 'undefined') {
                          splitted.created[type].push(object);
                      } else {
                          throw new ScriptingError(
                              `Id of object is defined "${object.id}"`
                              + 'but it absents in initial state',
                          );
                      }
                  }
              }
  
              // Now find deleted objects
              const indexes = {
                  shapes: exported.shapes.map((object) => +object.id),
                  tracks: exported.tracks.map((object) => +object.id),
                  tags: exported.tags.map((object) => +object.id),
              };
  
              for (const type of Object.keys(this.initialObjects)) {
                  for (const id of Object.keys(this.initialObjects[type])) {
                      if (!indexes[type].includes(+id)) {
                          const object = this.initialObjects[type][id];
                          splitted.deleted[type].push(object);
                      }
                  }
              }
  
  
              return splitted;
          }
  
          _updateCreatedObjects(saved, indexes) {
              const savedLength = saved.tracks.length
                  + saved.shapes.length + saved.tags.length;
  
              const indexesLength = indexes.tracks.length
                  + indexes.shapes.length + indexes.tags.length;
  
              if (indexesLength !== savedLength) {
                  throw new ScriptingError(
                      'Number of indexes is differed by number of saved objects'
                          + `${indexesLength} vs ${savedLength}`,
                  );
              }
  
              // Updated IDs of created objects
              for (const type of Object.keys(indexes)) {
                  for (let i = 0; i < indexes[type].length; i++) {
                      const clientID = indexes[type][i];
                      this.collection.objects[clientID].serverID = saved[type][i].id;
                      if (type === 'tracks') {
                          // We have to reset cache because of old value of serverID was saved there
                          this.collection.objects[clientID].resetCache();
                      }
                  }
              }
          }
  
          _receiveIndexes(exported) {
              // Receive client indexes before saving
              const indexes = {
                  tracks: exported.tracks.map((track) => track.clientID),
                  shapes: exported.shapes.map((shape) => shape.clientID),
                  tags: exported.tags.map((tag) => tag.clientID),
              };
  
              // Remove them from the request body
              exported.tracks.concat(exported.shapes).concat(exported.tags)
                  .map((value) => {
                      delete value.clientID;
                      return value;
                  });
  
              return indexes;
          }
  
          async save(onUpdate) {
              if (typeof onUpdate !== 'function') {
                  onUpdate = (message) => {
                      console.log(message);
                  };
              }
  
              try {
                  const exported = this.collection.export();
                  const { flush } = this.collection;
                  if (flush) {
                      onUpdate('New objects are being saved..');
                      const indexes = this._receiveIndexes(exported);
                      const savedData = await this._put({ ...exported, version: this.version });
                      this.version = savedData.version;
                      this.collection.flush = false;
  
                      onUpdate('Saved objects are being updated in the client');
                      this._updateCreatedObjects(savedData, indexes);
  
                      onUpdate('Initial state is being updated');
  
                      this._resetState();
                      for (const type of Object.keys(this.initialObjects)) {
                          for (const object of savedData[type]) {
                              this.initialObjects[type][object.id] = object;
                          }
                      }
                  } else {
                      const {
                          created,
                          updated,
                          deleted,
                      } = this._split(exported);
  
                      onUpdate('New objects are being saved..');
                      const indexes = this._receiveIndexes(created);
                      const createdData = await this._create({ ...created, version: this.version });
                      this.version = createdData.version;
  
                      onUpdate('Saved objects are being updated in the client');
                      this._updateCreatedObjects(createdData, indexes);
  
                      onUpdate('Initial state is being updated');
                      for (const type of Object.keys(this.initialObjects)) {
                          for (const object of createdData[type]) {
                              this.initialObjects[type][object.id] = object;
                          }
                      }
  
                      onUpdate('Changed objects are being saved..');
                      this._receiveIndexes(updated);
                      const updatedData = await this._update({ ...updated, version: this.version });
                      this.version = updatedData.version;
  
                      onUpdate('Initial state is being updated');
                      for (const type of Object.keys(this.initialObjects)) {
                          for (const object of updatedData[type]) {
                              this.initialObjects[type][object.id] = object;
                          }
                      }
  
                      onUpdate('Changed objects are being saved..');
                      this._receiveIndexes(deleted);
                      const deletedData = await this._delete({ ...deleted, version: this.version });
                      this._version = deletedData.version;
  
                      onUpdate('Initial state is being updated');
                      for (const type of Object.keys(this.initialObjects)) {
                          for (const object of deletedData[type]) {
                              delete this.initialObjects[type][object.id];
                          }
                      }
                  }
  
                  this.hash = this._getHash();
                  onUpdate('Saving is done');
              } catch (error) {
                  onUpdate(`Can not save annotations: ${error.message}`);
                  throw error;
              }
          }
  
          hasUnsavedChanges() {
              return this._getHash() !== this.hash;
          }
      }
  
      module.exports = AnnotationsSaver;
  })();
  