import { readFile, readFileSync } from 'fs';
import globby from 'globby';
import { relative } from 'path';
import { DEFAULT_FILE_ENCODING } from '../constants';
import { Disposable } from './disposable/disposable';
import { Disposer } from './disposable/disposer';
import { DeferredPromise } from './future/deferred-promise';
import { Logger } from './logging/logger';
import { normalizePath } from './utils';

const DEFAULT_GLOB_OPTIONS: globby.GlobbyOptions = {
  unique: true,
  absolute: true,
  baseNameMatch: false,
  onlyFiles: true,
  gitignore: true
};

export interface FileHandlerOptions extends globby.GlobbyOptions {
  cwd?: string;
  fileEncoding?: BufferEncoding;
}

export class FileHandler implements Disposable {
  private readonly fileHandlerOptions: FileHandlerOptions;
  private readonly fileEncoding: BufferEncoding;
  private readonly cwd: string;
  private readonly disposables: Disposable[] = [];

  public constructor(private readonly logger: Logger, fileHandlerOptions: FileHandlerOptions = {}) {
    this.disposables.push(logger);
    this.fileEncoding = fileHandlerOptions.fileEncoding ?? DEFAULT_FILE_ENCODING;
    this.cwd = fileHandlerOptions.cwd ?? process.cwd();

    this.fileHandlerOptions = {
      ...fileHandlerOptions,
      fileEncoding: this.fileEncoding,
      cwd: this.cwd
    };
  }

  public readFileSync(filePath: string, encoding?: BufferEncoding): string | undefined {
    this.logger.debug(() => `Reading file synchronously: ${filePath}`);
    let fileContents: string | undefined;

    try {
      fileContents = readFileSync(filePath, encoding ?? this.fileEncoding);
    } catch (error) {
      this.logger.error(() => `Failed reading file ${filePath}: ${error}`);
    }
    return fileContents;
  }

  public async readFile(filePath: string, encoding?: BufferEncoding): Promise<string> {
    this.logger.debug(() => `Reading file async: ${filePath}`);
    const deferredFileContents = new DeferredPromise<string>();

    readFile(filePath, encoding ?? this.fileEncoding, (error, data) => {
      if (error) {
        this.logger.error(() => `Failed reading file ${filePath}: ${error}`);
        deferredFileContents.reject(new Error(`Failed to read file '${filePath}': ${error}`));
      } else {
        this.logger.trace(() => `Done reading file ${filePath}: ${error}`);
        deferredFileContents.fulfill(data?.toString());
      }
    });
    return deferredFileContents.promise();
  }

  public async resolveFileGlobs(filePatterns: string[], globOptions: globby.GlobbyOptions = {}): Promise<string[]> {
    try {
      const searchOptions = { ...DEFAULT_GLOB_OPTIONS, ...this.fileHandlerOptions, ...globOptions };
      const files = (await globby(filePatterns, searchOptions)).map(file => normalizePath(file));

      this.logger.debug(() => `Resolved ${files.length} file(s) from file patterns: ${JSON.stringify(filePatterns)}`);

      this.logger.trace(
        () =>
          `List of resolved files from file patterns: ${JSON.stringify(filePatterns)} ` +
          `using options: ${JSON.stringify(searchOptions, null, 2)} ` +
          `are: ${JSON.stringify(files, null, 2)}`
      );
      return files;
    } catch (error) {
      const errorMsg = `Failed to resolve files from file patterns: ${JSON.stringify(filePatterns)}: \n${error}`;
      this.logger.error(() => errorMsg);
      throw new Error(errorMsg);
    }
  }

  public getFileRelativePath(fileAbsolutePath: string) {
    return normalizePath(relative(this.cwd, fileAbsolutePath));
  }

  public async dispose() {
    await Disposer.dispose(this.disposables);
  }
}