/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this file,
 * You can obtain one at http://mozilla.org/MPL/2.0/. */

/* eslint-env worker */

if (this.Components) {
  throw new Error("This worker can only be loaded from a worker thread");
}

// Worker thread for osfile asynchronous front-end

(function(exports) {
  "use strict";

  // Timestamps, for use in Telemetry.
  // The object is set to |null| once it has been sent
  // to the main thread.
  let timeStamps = {
    entered: Date.now(),
    loaded: null
  };

  importScripts("resource://gre/modules/osfile.jsm");

  let PromiseWorker = require("resource://gre/modules/workers/PromiseWorker.js");
  let SharedAll = require("resource://gre/modules/osfile/osfile_shared_allthreads.jsm");
  let LOG = SharedAll.LOG.bind(SharedAll, "Agent");

  let worker = new PromiseWorker.AbstractWorker();
  worker.dispatch = function(method, args = []) {
    return Agent[method](...args);
  },
  worker.log = LOG;
  worker.postMessage = function(message, ...transfers) {
    if (timeStamps) {
      message.timeStamps = timeStamps;
      timeStamps = null;
    }
    self.postMessage(message, ...transfers);
  };
  worker.close = function() {
    self.close();
  };
  let Meta = PromiseWorker.Meta;

  self.addEventListener("message", msg => worker.handleMessage(msg));

 /**
  * A data structure used to track opened resources
  */
  let ResourceTracker = function ResourceTracker() {
   // A number used to generate ids
   this._idgen = 0;
   // A map from id to resource
   this._map = new Map();
  };
  ResourceTracker.prototype = {
   /**
    * Get a resource from its unique identifier.
    */
   get(id) {
     let result = this._map.get(id);
     if (result == null) {
       return result;
     }
     return result.resource;
   },
   /**
    * Remove a resource from its unique identifier.
    */
   remove(id) {
     if (!this._map.has(id)) {
       throw new Error("Cannot find resource id " + id);
     }
     this._map.delete(id);
   },
   /**
    * Add a resource, return a new unique identifier
    *
    * @param {*} resource A resource.
    * @param {*=} info Optional information. For debugging purposes.
    *
    * @return {*} A unique identifier. For the moment, this is a number,
    * but this might not remain the case forever.
    */
   add(resource, info) {
     let id = this._idgen++;
     this._map.set(id, {resource, info});
     return id;
   },
   /**
    * Return a list of all open resources i.e. the ones still present in
    * ResourceTracker's _map.
    */
   listOpenedResources: function listOpenedResources() {
     return Array.from(this._map, ([id, resource]) => resource.info.path);
   }
  };

 /**
  * A map of unique identifiers to opened files.
  */
  let OpenedFiles = new ResourceTracker();

 /**
  * Execute a function in the context of a given file.
  *
  * @param {*} id A unique identifier, as used by |OpenFiles|.
  * @param {Function} f A function to call.
  * @param {boolean} ignoreAbsent If |true|, the error is ignored. Otherwise, the error causes an exception.
  * @return The return value of |f()|
  *
  * This function attempts to get the file matching |id|. If
  * the file exists, it executes |f| within the |this| set
  * to the corresponding file. Otherwise, it throws an error.
  */
  let withFile = function withFile(id, f, ignoreAbsent) {
   let file = OpenedFiles.get(id);
   if (file == null) {
     if (!ignoreAbsent) {
       throw OS.File.Error.closed("accessing file");
     }
     return undefined;
   }
   return f.call(file);
  };

  let OpenedDirectoryIterators = new ResourceTracker();
  let withDir = function withDir(fd, f, ignoreAbsent) {
   let file = OpenedDirectoryIterators.get(fd);
   if (file == null) {
     if (!ignoreAbsent) {
       throw OS.File.Error.closed("accessing directory");
     }
     return undefined;
   }
   if (!(file instanceof File.DirectoryIterator)) {
     throw new Error("file is not a directory iterator " + file.__proto__.toSource());
   }
   return f.call(file);
  };

  let Type = exports.OS.Shared.Type;

  let File = exports.OS.File;

 /**
  * The agent.
  *
  * It is in charge of performing method-specific deserialization
  * of messages, calling the function/method of OS.File and serializing
  * back the results.
  */
  let Agent = {
   // Update worker's OS.Shared.DEBUG flag message from controller.
   SET_DEBUG(aDEBUG) {
     SharedAll.Config.DEBUG = aDEBUG;
   },
   // Return worker's current OS.Shared.DEBUG value to controller.
   // Note: This is used for testing purposes.
   GET_DEBUG() {
     return SharedAll.Config.DEBUG;
   },
   /**
    * Execute shutdown sequence, returning data on leaked file descriptors.
    *
    * @param {bool} If |true|, kill the worker if this would not cause
    * leaks.
    */
   Meta_shutdown(kill) {
     let result = {
       openedFiles: OpenedFiles.listOpenedResources(),
       openedDirectoryIterators: OpenedDirectoryIterators.listOpenedResources(),
       killed: false // Placeholder
     };

     // Is it safe to kill the worker?
     let safe = result.openedFiles.length == 0
           && result.openedDirectoryIterators.length == 0;
     result.killed = safe && kill;

     return new Meta(result, {shutdown: result.killed});
   },
   // Functions of OS.File
   stat: function stat(path, options) {
     return exports.OS.File.Info.toMsg(
       exports.OS.File.stat(Type.path.fromMsg(path), options));
   },
   setPermissions: function setPermissions(path, options = {}) {
     return exports.OS.File.setPermissions(Type.path.fromMsg(path), options);
   },
   setDates: function setDates(path, accessDate, modificationDate) {
     return exports.OS.File.setDates(Type.path.fromMsg(path), accessDate,
                                     modificationDate);
   },
   getCurrentDirectory: function getCurrentDirectory() {
     return exports.OS.Shared.Type.path.toMsg(File.getCurrentDirectory());
   },
   setCurrentDirectory: function setCurrentDirectory(path) {
     File.setCurrentDirectory(exports.OS.Shared.Type.path.fromMsg(path));
   },
   copy: function copy(sourcePath, destPath, options) {
     return File.copy(Type.path.fromMsg(sourcePath),
       Type.path.fromMsg(destPath), options);
   },
   move: function move(sourcePath, destPath, options) {
     return File.move(Type.path.fromMsg(sourcePath),
       Type.path.fromMsg(destPath), options);
   },
   makeDir: function makeDir(path, options) {
     return File.makeDir(Type.path.fromMsg(path), options);
   },
   removeEmptyDir: function removeEmptyDir(path, options) {
     return File.removeEmptyDir(Type.path.fromMsg(path), options);
   },
   remove: function remove(path, options) {
     return File.remove(Type.path.fromMsg(path), options);
   },
   open: function open(path, mode, options) {
     let filePath = Type.path.fromMsg(path);
     let file = File.open(filePath, mode, options);
     return OpenedFiles.add(file, {
       // Adding path information to keep track of opened files
       // to report leaks when debugging.
       path: filePath
     });
   },
   openUnique: function openUnique(path, options) {
     let filePath = Type.path.fromMsg(path);
     let openedFile = OS.Shared.AbstractFile.openUnique(filePath, options);
     let resourceId = OpenedFiles.add(openedFile.file, {
       // Adding path information to keep track of opened files
       // to report leaks when debugging.
       path: openedFile.path
     });

     return {
       path: openedFile.path,
       file: resourceId
     };
   },
   read: function read(path, bytes, options) {
     let data = File.read(Type.path.fromMsg(path), bytes, options);
     if (typeof data == "string") {
       return data;
     }
     return new Meta({
         buffer: data.buffer,
         byteOffset: data.byteOffset,
         byteLength: data.byteLength
     }, {
       transfers: [data.buffer]
     });
   },
   exists: function exists(path) {
     return File.exists(Type.path.fromMsg(path));
   },
   writeAtomic: function writeAtomic(path, buffer, options) {
     if (options.tmpPath) {
       options.tmpPath = Type.path.fromMsg(options.tmpPath);
     }
     return File.writeAtomic(Type.path.fromMsg(path),
                             Type.voidptr_t.fromMsg(buffer),
                             options
                            );
   },
   removeDir(path, options) {
     return File.removeDir(Type.path.fromMsg(path), options);
   },
   new_DirectoryIterator: function new_DirectoryIterator(path, options) {
     let directoryPath = Type.path.fromMsg(path);
     let iterator = new File.DirectoryIterator(directoryPath, options);
     return OpenedDirectoryIterators.add(iterator, {
       // Adding path information to keep track of opened directory
       // iterators to report leaks when debugging.
       path: directoryPath
     });
   },
   // Methods of OS.File
   File_prototype_close: function close(fd) {
     return withFile(fd,
       function do_close() {
         try {
           return this.close();
         } finally {
           OpenedFiles.remove(fd);
         }
     });
   },
   File_prototype_stat: function stat(fd) {
     return withFile(fd,
       function do_stat() {
         return exports.OS.File.Info.toMsg(this.stat());
       });
   },
   File_prototype_setPermissions: function setPermissions(fd, options = {}) {
     return withFile(fd,
       function do_setPermissions() {
         return this.setPermissions(options);
       });
   },
   File_prototype_setDates: function setDates(fd, accessTime, modificationTime) {
     return withFile(fd,
       function do_setDates() {
         return this.setDates(accessTime, modificationTime);
       });
   },
   File_prototype_read: function read(fd, nbytes, options) {
     return withFile(fd,
       function do_read() {
         let data = this.read(nbytes, options);
         return new Meta({
             buffer: data.buffer,
             byteOffset: data.byteOffset,
             byteLength: data.byteLength
         }, {
           transfers: [data.buffer]
         });
       }
     );
   },
   File_prototype_readTo: function readTo(fd, buffer, options) {
     return withFile(fd,
       function do_readTo() {
         return this.readTo(exports.OS.Shared.Type.voidptr_t.fromMsg(buffer),
         options);
       });
   },
   File_prototype_write: function write(fd, buffer, options) {
     return withFile(fd,
       function do_write() {
         return this.write(exports.OS.Shared.Type.voidptr_t.fromMsg(buffer),
         options);
       });
   },
   File_prototype_setPosition: function setPosition(fd, pos, whence) {
     return withFile(fd,
       function do_setPosition() {
         return this.setPosition(pos, whence);
       });
   },
   File_prototype_getPosition: function getPosition(fd) {
     return withFile(fd,
       function do_getPosition() {
         return this.getPosition();
       });
   },
   File_prototype_flush: function flush(fd) {
     return withFile(fd,
       function do_flush() {
         return this.flush();
       });
   },
   // Methods of OS.File.DirectoryIterator
   DirectoryIterator_prototype_next: function next(dir) {
     return withDir(dir,
       function do_next() {
         let {value, done} = this.next();
         if (done) {
           OpenedDirectoryIterators.remove(dir);
           return {value: undefined, done: true};
         }
         return {value: File.DirectoryIterator.Entry.toMsg(value), done: false};
       }, false);
   },
   DirectoryIterator_prototype_nextBatch: function nextBatch(dir, size) {
     return withDir(dir,
       function do_nextBatch() {
         let result;
         try {
           result = this.nextBatch(size);
         } catch (x) {
           OpenedDirectoryIterators.remove(dir);
           throw x;
         }
         return result.map(File.DirectoryIterator.Entry.toMsg);
       }, false);
   },
   DirectoryIterator_prototype_close: function close(dir) {
     return withDir(dir,
       function do_close() {
         this.close();
         OpenedDirectoryIterators.remove(dir);
       }, true);// ignore error to support double-closing |DirectoryIterator|
   },
   DirectoryIterator_prototype_exists: function exists(dir) {
     return withDir(dir,
       function do_exists() {
         return this.exists();
       });
   }
  };
  if (!SharedAll.Constants.Win) {
    Agent.unixSymLink = function unixSymLink(sourcePath, destPath) {
      return File.unixSymLink(Type.path.fromMsg(sourcePath),
        Type.path.fromMsg(destPath));
    };
  }

  timeStamps.loaded = Date.now();
})(this);
