import * as cp from 'child_process';
import { EventEmitter } from 'events';
import * as iconv from 'iconv-lite-umd';
import { CancellationToken } from 'vscode';
import { dispose, IDisposable, toDisposable } from './disposableUtils';
import { onceEvent } from './eventUtils';
export interface IPijulOptions {
  path: string
  version: string
}
/**
 * The Pijul class holds information about the Pijul installation
 * in use by the extension which it uses for executing commands with
 * the Pijul CLI. All interactions with the CLI come through this class.
 */
export class Pijul {
  readonly path: string;
  readonly version: string;
  private readonly _onOutput = new EventEmitter();
  get onOutput (): EventEmitter { return this._onOutput; }
  constructor (options: IPijulOptions) {
    this.path = options.path;
    this.version = options.version;
  }
  /**
   * Opens an existing Pijul repository at the given location
   * @param repository The path to the root of the repository
   * @param dotPijul The path to the repository's .pijul directory
   */
  open (repository: string, dotPijul: string): Repository {
    return new Repository(this, repository, dotPijul);
  }
  /**
   * Runs the command to initialize a new pijul repository
   * @param repository The location where the new repository will be created
   */
  async init (repository: string): Promise<void> {
    await this.exec(repository, ['init']);
  }
  /**
   * Executes a new Pijul command in the given working directory.
   * @param cwd The current working directory in which the command will be run
   * @param args The arguments that will be passed to the command
   * @param options The options for the spawned child process
   */
  async exec (cwd: string, args: string[], options: SpawnOptions = {}): Promise<IExecutionResult<string>> {
    options = Object.assign({ cwd }, options);
    return await this._exec(args, options);
  }
  /**
   * Executes a new pijul command with the given arguments and decodes the execution result from a buffer into a string
   * @param args The arguments that will be passed to the command
   * @param options The options for the spawned child process
   */
  private async _exec (args: string[], options: SpawnOptions = {}): Promise<IExecutionResult<string>> {
    const child = this.spawn(args, options);
    if (options.onSpawn) {
      options.onSpawn(child);
    }
    if (options.input) {
      child.stdin!.end(options.input, 'utf8');
    }
    const bufferResult = await exec(child, options.cancellationToken);
    if (options.log !== false && bufferResult.stderr.length > 0) {
      this.log(`${bufferResult.stderr}\n`);
    }
    let encoding = options.encoding ?? 'utf8';
    encoding = iconv.encodingExists(encoding) ? encoding : 'utf8';
    const result: IExecutionResult<string> = {
      exitCode: bufferResult.exitCode,
      stdout: iconv.decode(bufferResult.stdout, encoding),
      stderr: bufferResult.stderr
    };
    if (bufferResult.exitCode) {
      return await Promise.reject<IExecutionResult<string>>(new Error('Error executing pijul command'));
    }
    return result;
  }
  /**
   * Spawn a Pijul process using the given arguments to utilize the Pijul CLI
   * @param args The arguments that will be passed to the child process
   * @param options The options that will be used to spawn the new process
   */
  private spawn (args: string[], options: SpawnOptions = {}): cp.ChildProcess {
    if (!this.path) {
      throw new Error('Pijul could not be found in the system.');
    }
    if (!options.stdio && !options.input) {
      options.stdio = ['ignore', null, null]; // Unless provided, ignore stdin and leave default streams for stdout and stderr
    }
    if (!options.log) {
      this.log(`> pijul ${args.join(' ')}\n`);
    }
    return cp.spawn(this.path, args, options);
  }
  /**
   * Emit a log message as an event
   * @param message The message to be emitted
   */
  private log (message: string): void {
    this._onOutput.emit('log', message);
  }
}
/**
 * The repository class represents a single Pijul repository.
 * It holds a reference to the Pijul installation which it uses
 * for interacting with the Pijul CLI. The Repository class exposes
 * functions for all of the Pijul CLI commands which require an
 * existing repository to run.
 */
export class Repository {
  constructor (
    private readonly _pijul: Pijul,
    private readonly repositoryRoot: string,
    readonly dotPijul: string
  ) {}
  get pijul (): Pijul {
    return this._pijul;
  }
  get root (): string {
    return this.repositoryRoot;
  }
}
/**
 * Handles the execution of a child process and the capture of its results, cancelling it if necessary
 * @param child The child process being executed
 * @param cancellationToken A cancellation token that can be used to cancel the child process
 */
async function exec (child: cp.ChildProcess, cancellationToken?: CancellationToken): Promise<IExecutionResult<Buffer>> {
  if (!child.stdout || !child.stderr) {
    throw new Error('Failed to get stdout or stderr from git process.');
  }
  if (cancellationToken?.isCancellationRequested) {
    throw new Error('Cancelled');
  }
  const disposables: IDisposable[] = [];
  // Create handles for stdout and stderr events
  const once = (ee: NodeJS.EventEmitter, name: string, fn: (...args: any[]) => void): void => {
    ee.once(name, fn);
    disposables.push(toDisposable(() => ee.removeListener(name, fn)));
  };
  const on = (ee: NodeJS.EventEmitter, name: string, fn: (...args: any[]) => void): void => {
    ee.on(name, fn);
    disposables.push(toDisposable(() => ee.removeListener(name, fn)));
  };
  // Create a promise representing all the different results of the child process
  let result = Promise.all<any>([
    new Promise<number>((resolve, reject) => {
      once(child, 'error', (err) => (reject(err)));
      once(child, 'exit', resolve);
    }),
    new Promise<Buffer>(resolve => {
      const buffers: Buffer[] = [];
      on(child.stdout!, 'data', (b: Buffer) => buffers.push(b));
      once(child.stdout!, 'close', () => resolve(Buffer.concat(buffers)));
    }),
    new Promise<string>(resolve => {
      const buffers: Buffer[] = [];
      on(child.stderr!, 'data', (b: Buffer) => buffers.push(b));
      once(child.stderr!, 'close', () => resolve(Buffer.concat(buffers).toString('utf8')));
    })
  ]) as Promise<[number, Buffer, string]>;
  if (cancellationToken) {
    const cancellationPromise = new Promise<[number, Buffer, string]>((resolve, reject) => {
      onceEvent(cancellationToken.onCancellationRequested)(() => {
        try {
          child.kill();
        } catch (err) {
          // Do nothing
        }
        reject(new Error('Cancelled'));
      });
    });
    result = Promise.race([result, cancellationPromise]);
  }
  try {
    const [exitCode, stdout, stderr] = await result;
    return { exitCode, stdout, stderr };
  } finally {
    dispose(disposables);
  }
}
/**
 * Interface for representing the results of the execution of a child process
 */
export interface IExecutionResult<T extends string | Buffer> {
  exitCode: number
  stdout: T
  stderr: string
}
/**
 * Interface for representing the options for spawning a child process
 */
export interface SpawnOptions extends cp.SpawnOptions {
  input?: string
  encoding?: string
  log?: boolean
  cancellationToken?: CancellationToken
  onSpawn?: (childProcess: cp.ChildProcess) => void
}