/**
 * @license
 * Copyright 2022 The Emscripten Authors
 * SPDX-License-Identifier: MIT
 */

addToLibrary({
  $wasmfsOPFSDirectoryHandles__deps: ['$HandleAllocator'],
  $wasmfsOPFSDirectoryHandles: 'new HandleAllocator()',
  $wasmfsOPFSFileHandles__deps: ['$HandleAllocator'],
  $wasmfsOPFSFileHandles: 'new HandleAllocator()',
  $wasmfsOPFSAccessHandles__deps: ['$HandleAllocator'],
  $wasmfsOPFSAccessHandles: 'new HandleAllocator()',
  $wasmfsOPFSBlobs__deps: ['$HandleAllocator'],
  $wasmfsOPFSBlobs: 'new HandleAllocator()',

#if !PTHREADS
  // OPFS will only be used on modern browsers that supports JS classes.
  $FileSystemAsyncAccessHandle: class {
    // This class implements the same interface as the sync version, but has
    // async reads and writes. Hopefully this will one day be implemented by the
    // platform so we can remove it.
    constructor(handle) {
      this.handle = handle;
    }
    async close() {}
    async flush() {}
    async getSize() {
      let file = await this.handle.getFile();
      return file.size;
    }
    async read(buffer, options = { at: 0 }) {
      let file = await this.handle.getFile();
      // The end position may be past the end of the file, but slice truncates
      // it.
      let slice = await file.slice(options.at, options.at + buffer.length);
      let fileBuffer = await slice.arrayBuffer();
      let array = new Uint8Array(fileBuffer);
      buffer.set(array);
      return array.length;
    }
    async write(buffer, options = { at: 0 }) {
      let writable = await this.handle.createWritable({keepExistingData: true});
      await writable.write({ type: 'write', position: options.at, data: buffer });
      await writable.close();
      return buffer.length;
    }
    async truncate(size) {
      let writable = await this.handle.createWritable({keepExistingData: true});
      await writable.truncate(size);
      await writable.close();
    }
  },

  $wasmfsOPFSCreateAsyncAccessHandle__deps: ['$FileSystemAsyncAccessHandle'],
  $wasmfsOPFSCreateAsyncAccessHandle: (fileHandle) => new FileSystemAsyncAccessHandle(fileHandle),
#endif

#if PTHREADS
  $wasmfsOPFSProxyFinish__deps: ['emscripten_proxy_finish'],
#endif
  $wasmfsOPFSProxyFinish: (ctx) => {
    // When using pthreads the proxy needs to know when the work is finished.
    // When used with JSPI the work will be executed in an async block so there
    // is no need to notify when done.
#if PTHREADS
    _emscripten_proxy_finish(ctx);
#endif
  },

  _wasmfs_opfs_init_root_directory__deps: ['$wasmfsOPFSDirectoryHandles', '$wasmfsOPFSProxyFinish'],
  _wasmfs_opfs_init_root_directory__async: 'auto',
  _wasmfs_opfs_init_root_directory: async (ctx) => {
    // allocated.length starts off as 1 since 0 is a reserved handle
    if (wasmfsOPFSDirectoryHandles.allocated.length == 1) {
      // Closure compiler errors on this as it does not recognize the OPFS
      // API yet, it seems. Unfortunately an existing annotation for this is in
      // the closure compiler codebase, and cannot be overridden in user code
      // (it complains on a duplicate type annotation), so just suppress it.
      /** @suppress {checkTypes} */
      let root = await navigator.storage.getDirectory();
      wasmfsOPFSDirectoryHandles.allocated.push(root);
    }
    wasmfsOPFSProxyFinish(ctx);
  },

  // Return the file ID for the file with `name` under `parent`, creating it if
  // it doesn't exist and `create` or otherwise return a negative error code
  // corresponding to the error.
  $wasmfsOPFSGetOrCreateFile__deps: ['$wasmfsOPFSDirectoryHandles',
                                     '$wasmfsOPFSFileHandles'],
  $wasmfsOPFSGetOrCreateFile: async (parent, name, create) => {
    let parentHandle = wasmfsOPFSDirectoryHandles.get(parent);
    let fileHandle;
    try {
      fileHandle = await parentHandle.getFileHandle(name, {create: create});
    } catch (e) {
      if (e.name === 'NotFoundError') {
        return -{{{ cDefs.EEXIST }}};
      }
      if (e.name === 'TypeMismatchError') {
        return -{{{ cDefs.EISDIR }}};
      }
#if ASSERTIONS
      err('unexpected error:', e, e.stack);
#endif
      return -{{{ cDefs.EIO }}};
    }
    return wasmfsOPFSFileHandles.allocate(fileHandle);
  },

  // Return the file ID for the directory with `name` under `parent`, creating
  // it if it doesn't exist and `create` or otherwise return a negative error
  // code corresponding to the error.
  $wasmfsOPFSGetOrCreateDir__deps: ['$wasmfsOPFSDirectoryHandles'],
  $wasmfsOPFSGetOrCreateDir: async (parent, name, create) => {
    let parentHandle = wasmfsOPFSDirectoryHandles.get(parent);
    let childHandle;
    try {
      childHandle =
          await parentHandle.getDirectoryHandle(name, {create: create});
    } catch (e) {
      if (e.name === 'NotFoundError') {
        return -{{{ cDefs.EEXIST }}};
      }
      if (e.name === 'TypeMismatchError') {
        return -{{{ cDefs.ENOTDIR }}};
      }
#if ASSERTIONS
      err('unexpected error:', e, e.stack);
#endif
      return -{{{ cDefs.EIO }}};
    }
    return wasmfsOPFSDirectoryHandles.allocate(childHandle);
  },

  _wasmfs_opfs_get_child__deps: ['$wasmfsOPFSGetOrCreateFile',
                                 '$wasmfsOPFSGetOrCreateDir', '$wasmfsOPFSProxyFinish'],
  _wasmfs_opfs_get_child__async: 'auto',
  _wasmfs_opfs_get_child: async (ctx, parent, namePtr, childTypePtr, childIDPtr) => {
    let name = UTF8ToString(namePtr);
    let childType = 1;
    let childID = await wasmfsOPFSGetOrCreateFile(parent, name, false);
    if (childID == -{{{ cDefs.EISDIR }}}) {
      childType = 2;
      childID = await wasmfsOPFSGetOrCreateDir(parent, name, false);
    }
    {{{ makeSetValue('childTypePtr', 0, 'childType', 'i32') }}};
    {{{ makeSetValue('childIDPtr', 0, 'childID', 'i32') }}};
    wasmfsOPFSProxyFinish(ctx);
  },

  _wasmfs_opfs_get_entries__deps: [
    '$wasmfsOPFSProxyFinish',
    '$stackSave',
    '$stackRestore',
    '_wasmfs_opfs_record_entry',
  ],
  _wasmfs_opfs_get_entries__async: 'auto',
  _wasmfs_opfs_get_entries: async (ctx, dirID, entriesPtr, errPtr) => {
    let dirHandle = wasmfsOPFSDirectoryHandles.get(dirID);

    // TODO: Use 'for await' once Acorn supports that.
    try {
      let iter = dirHandle.entries();
      for (let entry; entry = await iter.next(), !entry.done;) {
        let [name, child] = entry.value;
        let sp = stackSave();
        let namePtr = stringToUTF8OnStack(name);
        let type = child.kind == 'file' ?
            {{{ cDefs['File::DataFileKind'] }}} :
            {{{ cDefs['File::DirectoryKind'] }}};
          __wasmfs_opfs_record_entry(entriesPtr, namePtr, type)
        stackRestore(sp);
      }
    } catch {
      let err = -{{{ cDefs.EIO }}};
      {{{ makeSetValue('errPtr', 0, 'err', 'i32') }}};
    }
    wasmfsOPFSProxyFinish(ctx);
  },

  _wasmfs_opfs_insert_file__deps: ['$wasmfsOPFSGetOrCreateFile', '$wasmfsOPFSProxyFinish'],
  _wasmfs_opfs_insert_file__async: 'auto',
  _wasmfs_opfs_insert_file: async (ctx, parent, namePtr, childIDPtr) => {
    let name = UTF8ToString(namePtr);
    let childID = await wasmfsOPFSGetOrCreateFile(parent, name, true);
    {{{ makeSetValue('childIDPtr', 0, 'childID', 'i32') }}};
    wasmfsOPFSProxyFinish(ctx);
  },

  _wasmfs_opfs_insert_directory__deps: ['$wasmfsOPFSGetOrCreateDir', '$wasmfsOPFSProxyFinish'],
  _wasmfs_opfs_insert_directory__async: 'auto',
  _wasmfs_opfs_insert_directory: async (ctx, parent, namePtr, childIDPtr) => {
    let name = UTF8ToString(namePtr);
    let childID = await wasmfsOPFSGetOrCreateDir(parent, name, true);
    {{{ makeSetValue('childIDPtr', 0, 'childID', 'i32') }}};
    wasmfsOPFSProxyFinish(ctx);
  },

  _wasmfs_opfs_move_file__deps: ['$wasmfsOPFSFileHandles',
                                 '$wasmfsOPFSDirectoryHandles',
                                 '$wasmfsOPFSProxyFinish'],
  _wasmfs_opfs_move_file__async: 'auto',
  _wasmfs_opfs_move_file: async (ctx, fileID, newParentID, namePtr, errPtr) => {
    let name = UTF8ToString(namePtr);
    let fileHandle = wasmfsOPFSFileHandles.get(fileID);
    let newDirHandle = wasmfsOPFSDirectoryHandles.get(newParentID);
    try {
      await fileHandle.move(newDirHandle, name);
    } catch {
      let err = -{{{ cDefs.EIO }}};
      {{{ makeSetValue('errPtr', 0, 'err', 'i32') }}};
    }
    wasmfsOPFSProxyFinish(ctx);
  },

  _wasmfs_opfs_remove_child__deps: ['$wasmfsOPFSDirectoryHandles', '$wasmfsOPFSProxyFinish'],
  _wasmfs_opfs_remove_child__async: 'auto',
  _wasmfs_opfs_remove_child: async (ctx, dirID, namePtr, errPtr) => {
    let name = UTF8ToString(namePtr);
    let dirHandle = wasmfsOPFSDirectoryHandles.get(dirID);
    try {
      await dirHandle.removeEntry(name);
    } catch {
      let err = -{{{ cDefs.EIO }}};
      {{{ makeSetValue('errPtr', 0, 'err', 'i32') }}};
    }
    wasmfsOPFSProxyFinish(ctx);
  },

  _wasmfs_opfs_free_file__deps: ['$wasmfsOPFSFileHandles'],
  _wasmfs_opfs_free_file: (fileID) => {
    wasmfsOPFSFileHandles.free(fileID);
  },

  _wasmfs_opfs_free_directory__deps: ['$wasmfsOPFSDirectoryHandles'],
  _wasmfs_opfs_free_directory: (dirID) => {
    wasmfsOPFSDirectoryHandles.free(dirID);
  },

  _wasmfs_opfs_open_access__deps: ['$wasmfsOPFSFileHandles',
                                   '$wasmfsOPFSAccessHandles', '$wasmfsOPFSProxyFinish',
#if !PTHREADS
                                   '$wasmfsOPFSCreateAsyncAccessHandle'
#endif
                                  ],
  _wasmfs_opfs_open_access__async: 'auto',
  _wasmfs_opfs_open_access: async (ctx, fileID, accessIDPtr) => {
    let fileHandle = wasmfsOPFSFileHandles.get(fileID);
    let accessID;
    try {
      let accessHandle;
#if PTHREADS
      // TODO: Remove this once the Access Handles API has settled.
      // TODO: Closure is confused by this code that supports two versions of
      //       the same API, so suppress type checking on it.
      /** @suppress {checkTypes} */
      var len = FileSystemFileHandle.prototype.createSyncAccessHandle.length;
      if (len == 0) {
        accessHandle = await fileHandle.createSyncAccessHandle();
      } else {
        accessHandle = await fileHandle.createSyncAccessHandle(
            {mode: 'in-place'});
      }
#else
      accessHandle = await wasmfsOPFSCreateAsyncAccessHandle(fileHandle);
#endif
      accessID = wasmfsOPFSAccessHandles.allocate(accessHandle);
    } catch (e) {
      // TODO: Presumably only one of these will appear in the final API?
      if (e.name === 'InvalidStateError' ||
          e.name === 'NoModificationAllowedError') {
        accessID = -{{{ cDefs.EACCES }}};
      } else {
#if ASSERTIONS
        err('unexpected error:', e, e.stack);
#endif
        accessID = -{{{ cDefs.EIO }}};
      }
    }
    {{{ makeSetValue('accessIDPtr', 0, 'accessID', 'i32') }}};
    wasmfsOPFSProxyFinish(ctx);
  },

  _wasmfs_opfs_open_blob__deps: ['$wasmfsOPFSFileHandles',
                                 '$wasmfsOPFSBlobs', '$wasmfsOPFSProxyFinish'],
  _wasmfs_opfs_open_blob__async: 'auto',
  _wasmfs_opfs_open_blob: async (ctx, fileID, blobIDPtr) => {
    let fileHandle = wasmfsOPFSFileHandles.get(fileID);
    let blobID;
    try {
      let blob = await fileHandle.getFile();
      blobID = wasmfsOPFSBlobs.allocate(blob);
    } catch (e) {
      if (e.name === 'NotAllowedError') {
        blobID = -{{{ cDefs.EACCES }}};
      } else {
#if ASSERTIONS
        err('unexpected error:', e, e.stack);
#endif
        blobID = -{{{ cDefs.EIO }}};
      }
    }
    {{{ makeSetValue('blobIDPtr', 0, 'blobID', 'i32') }}};
    wasmfsOPFSProxyFinish(ctx);
  },

  _wasmfs_opfs_close_access__deps: ['$wasmfsOPFSAccessHandles', '$wasmfsOPFSProxyFinish'],
  _wasmfs_opfs_close_access__async: 'auto',
  _wasmfs_opfs_close_access: async (ctx, accessID, errPtr) => {
    let accessHandle = wasmfsOPFSAccessHandles.get(accessID);
    try {
      await accessHandle.close();
    } catch {
      let err = -{{{ cDefs.EIO }}};
      {{{ makeSetValue('errPtr', 0, 'err', 'i32') }}};
    }
    wasmfsOPFSAccessHandles.free(accessID);
    wasmfsOPFSProxyFinish(ctx);
  },

  _wasmfs_opfs_close_blob__deps: ['$wasmfsOPFSBlobs'],
  _wasmfs_opfs_close_blob: (blobID) => {
    wasmfsOPFSBlobs.free(blobID);
  },

  _wasmfs_opfs_read_access__i53abi: true,
  _wasmfs_opfs_read_access__deps: ['$wasmfsOPFSAccessHandles'],
  _wasmfs_opfs_read_access__async: 'auto',
  _wasmfs_opfs_read_access: {{{ asyncIf(!PTHREADS) }}}(accessID, bufPtr, len, pos) => {
    let accessHandle = wasmfsOPFSAccessHandles.get(accessID);
    let data = HEAPU8.subarray(bufPtr, bufPtr + len);
    try {
      return {{{ awaitIf(!PTHREADS) }}}accessHandle.read(data, {at: pos});
    } catch (e) {
      if (e.name == 'TypeError') {
        return -{{{ cDefs.EINVAL }}};
      }
#if ASSERTIONS
      err('unexpected error:', e, e.stack);
#endif
      return -{{{ cDefs.EIO }}};
    }
  },

  _wasmfs_opfs_read_blob__i53abi: true,
  _wasmfs_opfs_read_blob__deps: ['$wasmfsOPFSBlobs', '$wasmfsOPFSProxyFinish'],
  _wasmfs_opfs_read_blob__async: 'auto',
  _wasmfs_opfs_read_blob: async (ctx, blobID, bufPtr, len, pos, nreadPtr) => {
    let blob = wasmfsOPFSBlobs.get(blobID);
    let slice = blob.slice(pos, pos + len);
    let nread = 0;

    try {
      // TODO: Use ReadableStreamBYOBReader once
      // https://bugs.chromium.org/p/chromium/issues/detail?id=1189621 is
      // resolved.
      let buf = await slice.arrayBuffer();
      let data = new Uint8Array(buf);
      HEAPU8.set(data, bufPtr);
      nread += data.length;
    } catch (e) {
      if (e instanceof RangeError) {
        nread = -{{{ cDefs.EFAULT }}};
      } else {
#if ASSERTIONS
        err('unexpected error:', e, e.stack);
#endif
        nread = -{{{ cDefs.EIO }}};
      }
    }

    {{{ makeSetValue('nreadPtr', 0, 'nread', 'i32') }}};
    wasmfsOPFSProxyFinish(ctx);
  },

  _wasmfs_opfs_write_access__i53abi: true,
  _wasmfs_opfs_write_access__deps: ['$wasmfsOPFSAccessHandles'],
  _wasmfs_opfs_write_access__async: 'auto',
  _wasmfs_opfs_write_access: {{{ asyncIf(!PTHREADS) }}}(accessID, bufPtr, len, pos) => {
    let accessHandle = wasmfsOPFSAccessHandles.get(accessID);
    let data = HEAPU8.subarray(bufPtr, bufPtr + len);
    try {
      return {{{ awaitIf(!PTHREADS) }}}accessHandle.write(data, {at: pos});
    } catch (e) {
      if (e.name == 'TypeError') {
        return -{{{ cDefs.EINVAL }}};
      }
#if ASSERTIONS
      err('unexpected error:', e, e.stack);
#endif
      return -{{{ cDefs.EIO }}};
    }
  },

  _wasmfs_opfs_get_size_access__deps: ['$wasmfsOPFSAccessHandles', '$wasmfsOPFSProxyFinish'],
  _wasmfs_opfs_get_size_access__async: 'auto',
  _wasmfs_opfs_get_size_access: async (ctx, accessID, sizePtr) => {
    let accessHandle = wasmfsOPFSAccessHandles.get(accessID);
    let size;
    try {
      size = await accessHandle.getSize();
    } catch {
      size = -{{{ cDefs.EIO }}};
    }
    {{{ makeSetValue('sizePtr', 0, 'size', 'i64') }}};
    wasmfsOPFSProxyFinish(ctx);
  },

  _wasmfs_opfs_get_size_blob__i53abi: true,
  _wasmfs_opfs_get_size_blob__deps: ['$wasmfsOPFSBlobs'],
  _wasmfs_opfs_get_size_blob: (blobID) => {
    // This cannot fail.
	  return wasmfsOPFSBlobs.get(blobID).size;
  },

  _wasmfs_opfs_get_size_file__deps: ['$wasmfsOPFSFileHandles', '$wasmfsOPFSProxyFinish'],
  _wasmfs_opfs_get_size_file__async: 'auto',
  _wasmfs_opfs_get_size_file: async (ctx, fileID, sizePtr) => {
    let fileHandle = wasmfsOPFSFileHandles.get(fileID);
    let size;
    try {
      size = (await fileHandle.getFile()).size;
    } catch {
      size = -{{{ cDefs.EIO }}};
    }
    {{{ makeSetValue('sizePtr', 0, 'size', 'i64') }}};
    wasmfsOPFSProxyFinish(ctx);
  },

  _wasmfs_opfs_set_size_access__i53abi: true,
  _wasmfs_opfs_set_size_access__deps: ['$wasmfsOPFSAccessHandles', '$wasmfsOPFSProxyFinish'],
  _wasmfs_opfs_set_size_access__async: 'auto',
  _wasmfs_opfs_set_size_access: async (ctx, accessID, size, errPtr) => {
    let accessHandle = wasmfsOPFSAccessHandles.get(accessID);
    try {
      await accessHandle.truncate(size);
    } catch {
      let err = -{{{ cDefs.EIO }}};
      {{{ makeSetValue('errPtr', 0, 'err', 'i32') }}};
    }
    wasmfsOPFSProxyFinish(ctx);
  },

  _wasmfs_opfs_set_size_file__i53abi: true,
  _wasmfs_opfs_set_size_file__deps: ['$wasmfsOPFSFileHandles', '$wasmfsOPFSProxyFinish'],
  _wasmfs_opfs_set_size_file__async: 'auto',
  _wasmfs_opfs_set_size_file: async (ctx, fileID, size, errPtr) => {
    let fileHandle = wasmfsOPFSFileHandles.get(fileID);
    try {
      let writable = await fileHandle.createWritable({keepExistingData: true});
      await writable.truncate(size);
      await writable.close();
    } catch {
      let err = -{{{ cDefs.EIO }}};
      {{{ makeSetValue('errPtr', 0, 'err', 'i32') }}};
    }
    wasmfsOPFSProxyFinish(ctx);
  },

  _wasmfs_opfs_flush_access__deps: ['$wasmfsOPFSAccessHandles', '$wasmfsOPFSProxyFinish'],
  _wasmfs_opfs_flush_access__async: 'auto',
  _wasmfs_opfs_flush_access: async (ctx, accessID, errPtr) => {
    let accessHandle = wasmfsOPFSAccessHandles.get(accessID);
    try {
      await accessHandle.flush();
    } catch {
      let err = -{{{ cDefs.EIO }}};
      {{{ makeSetValue('errPtr', 0, 'err', 'i32') }}};
    }
    wasmfsOPFSProxyFinish(ctx);
  }
});
