import * as plugins from './plugins.js';
import * as fs from './fs.js';
import * as memory from './memory.js';

export interface ISmartfileConstructorOptions {
  path: string;
  contentBuffer: Buffer;
  base: string;
}

/**
 * an vinyl file compatible in memory file class
 */
export class SmartFile extends plugins.smartjson.Smartjson {
  // ======
  // STATIC
  // ======

  /**
   * creates a Smartfile from a filePath
   * @param filePath
   */
  public static async fromFilePath(filePath: string, baseArg: string = process.cwd()) {
    filePath = plugins.path.resolve(filePath);
    const fileBuffer = fs.toBufferSync(filePath);
    const smartfile = new SmartFile({
      contentBuffer: fileBuffer,
      base: baseArg,
      path: plugins.path.relative(baseArg, filePath),
    });
    return smartfile;
  }

  public static async fromBuffer(
    filePath: string,
    contentBufferArg: Buffer,
    baseArg: string = process.cwd()
  ) {
    const smartfile = new SmartFile({
      contentBuffer: contentBufferArg,
      base: baseArg,
      path: plugins.path.relative(baseArg, filePath),
    });

    return smartfile;
  }

  public static async fromString(
    filePath: string,
    contentStringArg: string,
    encodingArg: 'utf8' | 'binary',
    baseArg = process.cwd()
  ) {
    const smartfile = new SmartFile({
      contentBuffer: Buffer.from(contentStringArg, encodingArg),
      base: baseArg,
      path: plugins.path.relative(baseArg, filePath),
    });

    return smartfile;
  }

  public static async fromFoldedJson(foldedJsonArg: string) {
    return new SmartFile(plugins.smartjson.parse(foldedJsonArg));
  }

  /**
   * creates a Smartfile from a ReadableStream
   * @param stream a readable stream that provides file content
   * @param filePath the file path to associate with the content
   * @param baseArg the base path to use for the file
   */
  public static async fromStream(
    stream: plugins.stream.Readable,
    filePath: string,
    baseArg: string = process.cwd()
  ): Promise<SmartFile> {
    return new Promise<SmartFile>((resolve, reject) => {
      const chunks: Buffer[] = [];
      stream.on('data', (chunk) => chunks.push(Buffer.from(chunk)));
      stream.on('error', (error) => reject(error));
      stream.on('end', () => {
        const contentBuffer = Buffer.concat(chunks);
        const smartfile = new SmartFile({
          contentBuffer: contentBuffer,
          base: baseArg,
          path: plugins.path.relative(baseArg, filePath),
        });
        resolve(smartfile);
      });
    });
  }

  public static async fromUrl(urlArg: string) {
    const response = await plugins.smartrequest.getBinary(urlArg);
    const smartfile = await SmartFile.fromBuffer(urlArg, response.body);
    return smartfile;
  }

  // ========
  // INSTANCE
  // ========
  /**
   * the relative path of the file
   */
  @plugins.smartjson.foldDec()
  public path: string;

  /**
   * a parsed path
   */
  public get parsedPath(): plugins.path.ParsedPath {
    return plugins.path.parse(this.path);
  }
  public get absolutePath() {
    return plugins.path.join(this.base, this.path);
  }

  public get absoluteParsedPath() {
    return plugins.path.parse(this.absolutePath);
  }

  /**
   * the content of the file as Buffer
   */
  @plugins.smartjson.foldDec()
  public contentBuffer: Buffer;

  /**
   * The current working directory of the file
   * Note:this is similar to gulp and different from native node path base
   */
  @plugins.smartjson.foldDec()
  public base: string;

  /**
   * sync the file with disk
   */
  @plugins.smartjson.foldDec()
  public sync: boolean;

  /**
   * the constructor of Smartfile
   * @param optionsArg
   */

  constructor(optionsArg: ISmartfileConstructorOptions) {
    super();
    if (optionsArg.contentBuffer) {
      this.contentBuffer = optionsArg.contentBuffer;
    } else {
      console.log('created empty Smartfile?');
    }
    this.path = optionsArg.path;
    this.base = optionsArg.base;
  }

  /**
   * set contents from string
   * @param contentString
   */
  public setContentsFromString(contentString: string, encodingArg: 'utf8' | 'binary' = 'utf8') {
    this.contents = Buffer.from(contentString, encodingArg);
  }

  /**
   * write file to disk at its original location
   * Behaviours:
   * - no argument write to exactly where the file was picked up
   */
  public async write() {
    let writePath = plugins.smartpath.transform.makeAbsolute(this.path, this.base);
    console.log(`writing to ${writePath}`);
    await memory.toFs(this.contentBuffer, writePath);
  }

  /**
   * writes the file to path given as argument
   * note: if the path is not absolute, takes process.cwd() as base
   * @param filePathArg
   */
  public async writeToDiskAtPath(filePathArg: string) {
    if (!plugins.path.isAbsolute(filePathArg)) {
      filePathArg = plugins.path.join(process.cwd(), filePathArg);
    }
    await memory.toFs(this.contentBuffer, filePathArg);
  }

  /**
   * writes the file to a directory combined with the relative path portion
   * @param dirPathArg
   * @returns
   */
  public async writeToDir(dirPathArg: string) {
    dirPathArg = plugins.smartpath.transform.toAbsolute(dirPathArg) as string;
    const filePath = plugins.path.join(dirPathArg, this.path);
    await memory.toFs(this.contentBuffer, filePath);
    return filePath;
  }

  /**
   * read file from disk
   */
  public async read() {
    this.contentBuffer = await fs.toBuffer(plugins.path.join(this.base, this.path));
  }

  /**
   * deletes the file from disk at its original location
   */
  public async delete() {
    await fs.remove(plugins.path.join(this.base, this.path));
  }

  /**
   * Renames the file to the specified new name.
   * - Updates the `path` property with the new name.
   * - Writes the file to the new location if it exists on disk.
   * @param newName The new name of the file (including extension if applicable).
   * @param writeToDisk (optional) If true, also renames the file on the disk.
   * @returns The updated file path after renaming.
   */
  public async rename(newName: string, writeToDisk: boolean = false): Promise<string> {
    // Validate the new name
    if (!newName || typeof newName !== 'string') {
      throw new Error('Invalid new name provided.');
    }

    // Extract the directory path
    const oldFilePath = this.path;
    const dirPath = plugins.path.dirname(this.path);

    // Create the new file path
    const newFilePath = plugins.path.join(dirPath, newName);

    // Update the `path` property
    this.path = newFilePath;

    // Optionally write the renamed file to disk
    if (writeToDisk) {
      const oldAbsolutePath = plugins.smartpath.transform.makeAbsolute(oldFilePath, this.base);
      const newAbsolutePath = plugins.smartpath.transform.makeAbsolute(newFilePath, this.base);

      // Rename the file on disk
      await plugins.fsExtra.rename(oldAbsolutePath, newAbsolutePath);
    }

    // Return the new path
    return this.path;
  }

  // -----------------------------------------------
  // vinyl compatibility
  // -----------------------------------------------
  /**
   * vinyl-compatibility: alias of this.contentBuffer
   */
  get contents(): Buffer {
    return this.contentBuffer;
  }
  set contents(contentsArg) {
    this.contentBuffer = contentsArg;
  }

  /**
   * vinyl-compatibility
   */
  public get cwd() {
    return process.cwd();
  }

  /**
   * return relative path of file
   */
  public get relative(): string {
    return this.path;
  }

  /**
   * return truw when the file has content
   */
  public isNull(): boolean {
    if (!this.contentBuffer) {
      return true;
    }
    return false;
  }

  /**
   * return true if contents are Buffer
   */
  public isBuffer(): boolean {
    if (this.contents instanceof Buffer) {
      return true;
    }
    return false;
  }

  public isDirectory() {
    return false;
  }

  public isStream() {
    return false;
  }

  public isSymbolic() {
    return false;
  }

  public async getHash(typeArg: 'path' | 'content' | 'all' = 'all') {
    const pathHash = await plugins.smarthash.sha256FromString(this.path);
    const contentHash = await plugins.smarthash.sha256FromBuffer(this.contentBuffer);
    const combinedHash = await plugins.smarthash.sha256FromString(pathHash + contentHash);
    switch (typeArg) {
      case 'path':
        return pathHash;
      case 'content':
        return contentHash;
      case 'all':
      default:
        return combinedHash;
    }
  }

  // update things
  public updateFileName(fileNameArg: string) {
    const oldFileName = this.parsedPath.base;
    this.path = this.path.replace(new RegExp(oldFileName + '$'), fileNameArg);
  }

  public async editContentAsString(editFuncArg: (fileStringArg: string) => Promise<string>) {
    const newFileString = await editFuncArg(this.contentBuffer.toString());
    this.contentBuffer = Buffer.from(newFileString);
  }

  /**
   * Returns a ReadableStream from the file's content buffer
   */
  public getStream(): plugins.stream.Readable {
    const stream = new plugins.stream.Readable();
    stream.push(this.contentBuffer); // Push the content buffer to the stream
    stream.push(null); // Push null to signify the end of the stream (EOF)
    return stream;
  }

  /**
   * Returns the size of the file in bytes
   */
  public async getSize(): Promise<number> {
    return this.contentBuffer.length;
  }
}
