import * as plugins from './plugins.js';
import * as helpers from './helpers.js';
import * as interfaces from './interfaces.js';
import { Directory } from './classes.directory.js';
import { MetaData } from './classes.metadata.js';

/**
 * represents a file in a directory
 */
export class File {
  // STATIC

  /**
   * creates a file in draft mode
   * you need to call .save() to store it in object storage
   * @param optionsArg
   */
  public static async create(optionsArg: {
    directory: Directory;
    name: string;
    contents: Buffer | string | plugins.stream.Readable;
    /**
     * if contents are of type string, you can specify the encoding here
     */
    encoding?: 'utf8' | 'binary';
  }): Promise<File> {
    const contents =
      typeof optionsArg.contents === 'string'
        ? Buffer.from(optionsArg.contents, optionsArg.encoding)
        : optionsArg.contents;
    const file = new File({
      directoryRefArg: optionsArg.directory,
      fileName: optionsArg.name,
    });
    if (contents instanceof plugins.stream.Readable) {
      await optionsArg.directory.fastPutStream({
        path: optionsArg.name,
        stream: contents,
      });
    } else {
      await optionsArg.directory.fastPut({
        path: optionsArg.name,
        contents: contents,
      });
    }
    return file;
  }

  // INSTANCE
  public parentDirectoryRef: Directory;
  public name: string;

  /**
   * get the full path to the file
   * @returns the full path to the file
   */
  public getBasePath(): string {
    return plugins.path.join(this.parentDirectoryRef.getBasePath(), this.name);
  }

  constructor(optionsArg: { directoryRefArg: Directory; fileName: string }) {
    this.parentDirectoryRef = optionsArg.directoryRefArg;
    this.name = optionsArg.fileName;
  }

  public async getContentsAsString(): Promise<string> {
    const fileBuffer = await this.getContents();
    return fileBuffer.toString();
  }

  public async getContents(): Promise<Buffer> {
    const resultBuffer = await this.parentDirectoryRef.bucketRef.fastGet({
      path: this.getBasePath(),
    });
    return resultBuffer;
  }

  public async getReadStream(typeArg: 'webstream'): Promise<ReadableStream>;
  public async getReadStream(typeArg: 'nodestream'): Promise<plugins.stream.Readable>;
  public async getReadStream(
    typeArg: 'nodestream' | 'webstream'
  ): Promise<ReadableStream | plugins.stream.Readable> {
    const readStream = this.parentDirectoryRef.bucketRef.fastGetStream(
      {
        path: this.getBasePath(),
      },
      typeArg as any
    );
    return readStream;
  }

  /**
   * deletes this file
   */
  public async delete(optionsArg?: { mode: 'trash' | 'permanent' }) {
    optionsArg = {
      ...{
        mode: 'permanent',
      },
      ...optionsArg,
    };

    if (optionsArg.mode === 'permanent') {
      await this.parentDirectoryRef.bucketRef.fastRemove({
        path: this.getBasePath(),
      });
      if (!this.name.endsWith('.metadata')) {
        if (await this.hasMetaData()) {
          const metadata = await this.getMetaData();
          await metadata.metadataFile.delete(optionsArg);
        }
      }
    } else if (optionsArg.mode === 'trash') {
      const metadata = await this.getMetaData();
      await metadata.storeCustomMetaData({
        key: 'recycle',
        value: {
          deletedAt: Date.now(),
          originalPath: this.getBasePath(),
        },
      });
      const trash = await this.parentDirectoryRef.bucketRef.getTrash();
      const trashDir = await trash.getTrashDir();
      await this.move({
        directory: trashDir,
        path: await trash.getTrashKeyByOriginalBasePath(this.getBasePath()),
      });
    }

    await this.parentDirectoryRef.listFiles();
  }

  /**
   * restores
   */
  public async restore(optionsArg: {
    useOriginalPath?: boolean;
    toPath?: string;
    overwrite?: boolean;
  } = {}) {
    optionsArg = {
      useOriginalPath: (() => {
        return optionsArg.toPath ? false : true;
      })(),
      overwrite: false,
      ...optionsArg,
    };
    const metadata = await this.getMetaData();
    const moveToPath = optionsArg.toPath || (await metadata.getCustomMetaData({
      key: 'recycle'
    })).originalPath;
    await metadata.deleteCustomMetaData({
      key: 'recycle'
    })
    await this.move({
      path: moveToPath,
    });
  }

  /**
   * allows locking the file
   * @param optionsArg
   */
  public async lock(optionsArg?: { timeoutMillis?: number }) {
    const metadata = await this.getMetaData();
    await metadata.setLock({
      lock: 'locked',
      expires: Date.now() + (optionsArg?.timeoutMillis || 1000),
    });
  }

  /**
   * actively unlocks a file
   *
   */
  public async unlock(optionsArg?: {
    /**
     * unlock the file even if not locked from this instance
     */
    force?: boolean;
  }) {
    const metadata = await this.getMetaData();
    await metadata.removeLock({
      force: optionsArg?.force || false,
    });
  }

  public async updateWithContents(optionsArg: {
    contents: Buffer | string | plugins.stream.Readable | ReadableStream;
    encoding?: 'utf8' | 'binary';
  }) {
    if (
      optionsArg.contents instanceof plugins.stream.Readable ||
      optionsArg.contents instanceof ReadableStream
    ) {
      await this.parentDirectoryRef.bucketRef.fastPutStream({
        path: this.getBasePath(),
        readableStream: optionsArg.contents,
        overwrite: true,
      });
    } else if (Buffer.isBuffer(optionsArg.contents)) {
      await this.parentDirectoryRef.bucketRef.fastPut({
        path: this.getBasePath(),
        contents: optionsArg.contents,
        overwrite: true,
      });
    } else if (typeof optionsArg.contents === 'string') {
      await this.parentDirectoryRef.bucketRef.fastPut({
        path: this.getBasePath(),
        contents: Buffer.from(optionsArg.contents, optionsArg.encoding),
        overwrite: true,
      });
    }
  }

  /**
   * moves the file to another directory
   */
  public async move(pathDescriptorArg: interfaces.IPathDecriptor) {
    let moveToPath: string = '';
    const isDirectory = await this.parentDirectoryRef.bucketRef.isDirectory(pathDescriptorArg);
    if (isDirectory) {
      moveToPath = await helpers.reducePathDescriptorToPath({
        ...pathDescriptorArg,
        path: plugins.path.join(pathDescriptorArg.path!, this.name),
      });
    } else {
      moveToPath = await helpers.reducePathDescriptorToPath(pathDescriptorArg);
    }
    // lets move the file
    await this.parentDirectoryRef.bucketRef.fastMove({
      sourcePath: this.getBasePath(),
      destinationPath: moveToPath,
      overwrite: true,
    });
    // lets move the metadatafile
    if (!this.name.endsWith('.metadata')) {
      const metadata = await this.getMetaData();
      await this.parentDirectoryRef.bucketRef.fastMove({
        sourcePath: metadata.metadataFile.getBasePath(),
        destinationPath: moveToPath + '.metadata',
        overwrite: true,
      });
    }

    // lets update references of this
    const baseDirectory = await this.parentDirectoryRef.bucketRef.getBaseDirectory();
    this.parentDirectoryRef = await baseDirectory.getSubDirectoryByName(
      await helpers.reducePathDescriptorToPath(pathDescriptorArg),
      {
        couldBeFilePath: true,
      }
    );
    this.name = pathDescriptorArg.path!;
  }

  public async hasMetaData(): Promise<boolean> {
    if (!this.name.endsWith('.metadata')) {
      const hasMetadataBool = MetaData.hasMetaData({
        file: this,
      });
      return hasMetadataBool;
    } else {
      return false;
    }
  }

  /**
   * allows updating the metadata of a file
   * @param updatedMetadata
   */
  public async getMetaData() {
    if (this.name.endsWith('.metadata')) {
      throw new Error('metadata files cannot have metadata');
    }
    const metadata = await MetaData.createForFile({
      file: this,
    });
    return metadata;
  }

  /**
   * gets the contents as json
   */
  public async getJsonData() {
    const json = await this.getContentsAsString();
    const parsed = await JSON.parse(json);
    return parsed;
  }

  public async writeJsonData(dataArg: any) {
    await this.updateWithContents({
      contents: JSON.stringify(dataArg),
    });
  }

  public async getMagicBytes(optionsArg: { length: number }): Promise<Buffer> {
    return this.parentDirectoryRef.bucketRef.getMagicBytes({
      path: this.getBasePath(),
      length: optionsArg.length,
    });
  }
}
