import os from 'os';
import path from 'path';
import fs from '../fs';

/**
 * link your code to system commands
 */
export default class Linker {
  private readonly currentOs: NodeJS.Platform;

  private readonly startCommand: string;

  private fileMode = 0o744;

  constructor(startCommand: string) {
    this.currentOs = os.platform();
    this.startCommand = startCommand;
  }

  /**
   *
   * @param binPath
   * @param command it could be checkstyle or checkstyle@v0.0.5
   * @param name    always checkstyle, use command when it does not exist
   */
  public async register(binPath: string, command: string, name?: string) {
    if (this.currentOs === 'win32') {
      return this.linkToWin32(binPath, command, name);
    }
    return this.linkToUnixLike(binPath, command, name);
  }

  /**
   * 注册自定义指令
   * @param binPath
   * @param commands
   * @param name    always checkstyle, use command when it does not exist
   */
  public registerCustom(binPath: string, commands: string[], name: string) {
    if (this.currentOs === 'win32') {
      return this.linkCustomToWin32(binPath, commands, name);
    }
    return this.linkCustomToUnixLike(binPath, commands, name);
  }


  private async linkCustomToWin32(binPath: string, commands: string[], name: string) {
    await this.enableDir(binPath);
    const file = this.cmdFile(binPath, name);
    const template = this.customCmdTemplate(commands);
    await this.writeExecFile(file, template);
  }


  private async linkCustomToUnixLike(
    binPath: string,
    commands: string[],
    name: string,
  ) {
    await this.enableDir(binPath);
    const template = this.customShellTemplate(commands);
    const command = path.join(binPath, name);
    await this.writeExecFile(command, template);
  }

  private customShellTemplate(commands: string[]): string {
    let shell = '';
    // Note: 使用单条命令时，参数直接透传
    // 使用多命令时，由用户自行决定参数透传到哪条命令
    if (commands.length === 1) {
      shell = `${commands[0]} "$@"`;
    } else {
      shell = commands.join('\n');
    }
    return `#!/bin/sh\n${shell}`;
  }

  private customCmdTemplate(commands: string[]): string {
    let shell = '';
    // Note: 使用单条命令时，参数直接透传
    // 使用多命令时，由用户自行决定参数透传到哪条命令
    if (commands.length === 1) {
      shell = `${commands[0]} "%*"`;
    } else {
      shell = commands.join('\n');
    }
    return `@echo off\n${shell}`;
  }

  private remove(binPath: string, name: string) {
    if (this.currentOs === 'win32') {
      return this.removeOnWin32(binPath, name);
    }
    return this.removeOnUnixLike(binPath, name);
  }

  private removeOnWin32(binPath: string, name: string) {
    const cmdFile = this.cmdFile(binPath, name);
    return fs.unlink(cmdFile);
  }

  private async removeOnUnixLike(binPath: string, name: string) {
    const command = path.join(binPath, name);
    return fs.unlink(command);
  }

  private async linkToWin32(binPath: string, command: string, name?: string) {
    const file = this.cmdFile(binPath, name || command);
    await this.enableDir(binPath);
    const template = this.cmdTemplate(command);
    return this.writeExecFile(file, template);
  }

  private async linkToUnixLike(
    binPath: string,
    command: string,
    name?: string,
  ) {
    await this.enableDir(binPath);
    const commandFile = path.join(binPath, name || command);
    const template = this.shellTemplate(command);
    return this.writeExecFile(commandFile, template);
  }

  private async link(source: string, target: string) {
    try {
      await fs.unlink(target);
    } catch (e) {
    }
    return fs.symlink(source, target);
  }

  private writeExecFile(file: string, content: string) {
    return fs.writeFile(file, content, {
      mode: this.fileMode,
      flag: 'w',
      encoding: 'utf8',
    });
  }

  private shellTemplate(command: string): string {
    return `#!/bin/sh\n${this.startCommand} ${command} "$@"`;
  }

  private cmdTemplate(command: string): string {
    return `@echo off\n${this.startCommand} ${command} %*`;
  }

  private cmdFile(binPath: string, name: string): string {
    return path.join(binPath, `${name}.cmd`);
  }

  private async enableDir(...dirs: string[]) {
    if (!dirs) {
      return;
    }
    return Promise.all(dirs.map(async (dir) => {
      try {
        await fs.access(dir, fs.constants.F_OK);
      } catch (e) {
        return fs.mkdir(dir, { recursive: true });
      }
    }));
  }
}
