import { deserialize, serialize } from '../common/serializer.js';


/**
 * The base class of other exceptions in this module.
 */
export class ObjectFileSystemError extends Error {
  /**
   * @param {string} message
   * @param {{ cause?: unknown }} options
   */
  constructor(message, options={}) {
      super(message, options);
      Object.defineProperty(this, 'name', {
          configurable: true,
          enumerable: false,
          value: this.constructor.name,
          writable: true,
      });
      if('captureStackTrace' in Error) {
          Error.captureStackTrace(this, this.constructor);
      }
  }
}

export class InvalidPathObjectFileSystemError extends ObjectFileSystemError {
  /**
   * @param {string} path
   */
  constructor(path) {
    super(`Invalid path: "${path}"`);
  }
}

export class FileNotFoundObjectFileSystemError extends ObjectFileSystemError {
  /**
   * 
   * @param {string} path
   * @param {Error} cause
   */
  constructor(path, cause) {
    super(`File not found: "${path}"`, { cause });
  }
}

export class NotAllowedObjectFileSystemError extends ObjectFileSystemError {
  /**
   * @param {Error} cause
   */
  constructor(cause) {
    super(`Not allowed`, { cause });
  }
}

export function useObjectFileSystem() {
  const storageManager = navigator.storage;

  /**
   * @param {string} path
   * @returns {boolean}
   */
  function isValidFilePath(path) {
    if(!path.startsWith('/')) return false;
    if(path.endsWith('/')) return false;
    return true;
  }

  /**
   * @param {string} path
   * @param {Object} options
   * @param {boolean} options.create
   * @returns {FileSystemFileHandle}
   */
  async function traverseFile(path, { create=false }={}) {
    if(!isValidFilePath(path)) {
      throw new InvalidPathObjectFileSystemError(path);
    }

    try {
      let directoryHandle = await storageManager.getDirectory();
      const parts = path.slice(1).split('/');
      for(const name of parts.slice(0, -1)) {
        directoryHandle = await directoryHandle.getDirectoryHandle(
          name, { create }
        );
      }
      return await directoryHandle.getFileHandle(
        parts.slice(-1)[0], { create }
      );
    } catch(e) {
      if(e instanceof DOMException) {
        if(e.name === 'NotAllowedError') {
          throw new NotAllowedObjectFileSystemError(e);
        } else if(e.name === 'TypeMismatchError') {
          throw new FileNotFoundObjectFileSystemError(path, e);
        } else if(e.name === 'NotFoundError') {
          throw new FileNotFoundObjectFileSystemError(path, e);
        }
      }
      throw e;
    }
  }

  /**
   * @param {string} path
   * @returns {any}
   */
  async function get(path) {
    const fileHandle = await traverseFile(path);

    /** @type {File | null} */
    let file = null;
    try {
      file = await fileHandle.getFile();
    } catch(e) {
      if(e instanceof DOMException) {
        if(e.name === 'NotAllowedError') {
          throw new NotAllowedObjectFileSystemError(e);
        }
      }
      throw e;
    }

    return deserialize(JSON.parse(await file.text()));
  }

  /**
   * @param {string} path
   * @param {unknown} obj
   */
  async function put(path, obj) {
    const fileHandle = await traverseFile(path, { create: true });

    let writable = null;
    try {
      writable = await fileHandle.createWritable();
    } catch(e) {
      if(e instanceof DOMException) {
        if(e.name === 'NotAllowedError') {
          throw new NotAllowedObjectFileSystemError(e);
        }
      }
      throw e;
    }
    await writable.write(JSON.stringify(serialize(obj)));
    await writable.close();
  }

  return { get, put };
}
