/**
 * @license
 * Copyright Google LLC All Rights Reserved.
 *
 * Use of this source code is governed by an MIT-style license that can be
 * found in the LICENSE file at https://angular.dev/license
 */

import {
  Path,
  PathFragment,
  basename,
  dirname,
  join,
  normalize,
  relative,
  virtualFs,
} from '@angular-devkit/core';
import { NodeJsSyncHost } from '@angular-devkit/core/node';
import { Stats } from 'node:fs';
import {
  EMPTY,
  Observable,
  concatMap,
  delay,
  finalize,
  from,
  map,
  mergeMap,
  of,
  retry,
  tap,
} from 'rxjs';

/**
 * @deprecated
 */
export class TestProjectHost extends NodeJsSyncHost {
  private _currentRoot: Path | null = null;
  private _scopedSyncHost: virtualFs.SyncDelegateHost<Stats> | null = null;

  constructor(protected _templateRoot: Path) {
    super();
  }

  root(): Path {
    if (this._currentRoot === null) {
      throw new Error('TestProjectHost must be initialized before being used.');
    }

    return this._currentRoot;
  }

  scopedSync(): virtualFs.SyncDelegateHost<Stats> {
    if (this._currentRoot === null || this._scopedSyncHost === null) {
      throw new Error('TestProjectHost must be initialized before being used.');
    }

    return this._scopedSyncHost;
  }

  initialize(): Observable<void> {
    const recursiveList = (path: Path): Observable<Path> =>
      this.list(path).pipe(
        // Emit each fragment individually.
        concatMap((fragments) => from(fragments)),
        // Join the path with fragment.
        map((fragment) => join(path, fragment)),
        // Emit directory content paths instead of the directory path.
        mergeMap((path) =>
          this.isDirectory(path).pipe(
            concatMap((isDir) => (isDir ? recursiveList(path) : of(path))),
          ),
        ),
      );

    // Find a unique folder that we can write to use as current root.
    return this.findUniqueFolderPath().pipe(
      // Save the path and create a scoped host for it.
      tap((newFolderPath) => {
        this._currentRoot = newFolderPath;
        this._scopedSyncHost = new virtualFs.SyncDelegateHost(
          new virtualFs.ScopedHost(this, this.root()),
        );
      }),
      // List all files in root.
      concatMap(() => recursiveList(this._templateRoot)),
      // Copy them over to the current root.
      concatMap((from) => {
        const to = join(this.root(), relative(this._templateRoot, from));

        return this.read(from).pipe(concatMap((buffer) => this.write(to, buffer)));
      }),
      map(() => {}),
    );
  }

  restore(): Observable<void> {
    if (this._currentRoot === null) {
      return EMPTY;
    }

    // Delete the current root and clear the variables.
    // Wait 50ms and retry up to 10 times, to give time for file locks to clear.
    return this.exists(this.root()).pipe(
      delay(50),
      concatMap((exists) => (exists ? this.delete(this.root()) : EMPTY)),
      retry(10),
      finalize(() => {
        this._currentRoot = null;
        this._scopedSyncHost = null;
      }),
    );
  }

  writeMultipleFiles(files: { [path: string]: string | ArrayBufferLike | Buffer }): void {
    Object.keys(files).forEach((fileName) => {
      let content = files[fileName];
      if (typeof content == 'string') {
        content = virtualFs.stringToFileBuffer(content);
      } else if (content instanceof Buffer) {
        content = content.buffer.slice(content.byteOffset, content.byteOffset + content.byteLength);
      }

      this.scopedSync().write(normalize(fileName), content);
    });
  }

  replaceInFile(path: string, match: RegExp | string, replacement: string) {
    const content = virtualFs.fileBufferToString(this.scopedSync().read(normalize(path)));
    this.scopedSync().write(
      normalize(path),
      virtualFs.stringToFileBuffer(content.replace(match, replacement)),
    );
  }

  appendToFile(path: string, str: string) {
    const content = virtualFs.fileBufferToString(this.scopedSync().read(normalize(path)));
    this.scopedSync().write(normalize(path), virtualFs.stringToFileBuffer(content.concat(str)));
  }

  fileMatchExists(dir: string, regex: RegExp): PathFragment | undefined {
    const [fileName] = this.scopedSync()
      .list(normalize(dir))
      .filter((name) => name.match(regex));

    return fileName || undefined;
  }

  copyFile(from: string, to: string) {
    const content = this.scopedSync().read(normalize(from));
    this.scopedSync().write(normalize(to), content);
  }

  private findUniqueFolderPath(): Observable<Path> {
    // 11 character alphanumeric string.
    const randomString = Math.random().toString(36).slice(2);
    const newFolderName = `test-project-host-${basename(this._templateRoot)}-${randomString}`;
    const newFolderPath = join(dirname(this._templateRoot), newFolderName);

    return this.exists(newFolderPath).pipe(
      concatMap((exists) => (exists ? this.findUniqueFolderPath() : of(newFolderPath))),
    );
  }
}
