import * as path from 'path';
import * as fse from 'fs-extra';

/**
 *
 * separator
 * If the specified filename exists, the separator will be added before the incremental value such as: file{separator}2.jpg
 * The default value is '-'.
 *
 * mode
 * The mode allows you to specify which characters to use to generate the incremental value (the string after the separator)
 * The default value is 'numeric'.
 * 'numeric' Using the following characters: 1234567890
 * 'alpha' Using the following characters: abcdefghijklmnopqrstuvwxyz
 * 'ALPHA' Using the following characters: ABCDEFGHIJKLMNOPQRSTUVWXYZ
 * 'alphanumeric' Using the following characters: 0123456789abcdefghijklmnopqrstuvwxyz
 * 'ALPHANUMERIC' Using the following characters: 0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ
 * 'charset' You must specify the characters you wish to use in the charset option
 *
 * paddingCharacter && paddingSize
 * If you wish to left-pad the incremental values with a character, use this option. Here's an example :
 * var uniquefilename = require('uniquefilename');
 * options = {mode: 'alpha', paddingCharacter: '0', paddingSize: 3};
 * uniquefilename.get('/path/to/dir/file.jpg', options, function(filename) {
 *   // filename might be "/path/to/dir/file.jpg",
 *   // "/path/to/dir/file-002.jpg", "/path/to/dir/file-045.jpg", etc...
 *   // depending on the files that exist on your filesystem
 * });
 *
 * alwaysAppend
 * If alwaysAppend is true filenames will include the separator and attachment from the first request.
 * So instead of file.jpg, file-2.jpg you'd get file-1.jpg, file-2.jpg.
 */
export interface UniqueFileNameOption {
  separator?: string;
  mode?:
    | 'numeric'
    | 'alpha'
    | 'ALPHA'
    | 'alphanumeric'
    | 'ALPHANUMERIC'
    | 'charset';
  paddingCharacter?: string;
  paddingSize?: number;
  alwaysAppend?: boolean;
  charset?: string;
}

const charsets = {
  alpha: 'abcdefghijklmnopqrstuvwxyz',
  alphanumeric: '0123456789abcdefghijklmnopqrstuvwxyz',
  ALPHA: 'ABCDEFGHIJKLMNOPQRSTUVWXYZ',
  ALPHANUMERIC: '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ'
};

interface UniqueFile {
  dir?: string;
  ext?: string;
  base?: string;
  increment?: number;
}

export class FileUtil {
  static blobToBuffer(blob: Blob): Promise<Buffer> {
    if (typeof Blob === 'undefined' || !(blob instanceof Blob)) {
      throw new Error('first argument must be a Blob');
    }

    return new Promise<Buffer>((resolve, reject) => {
      const reader = new FileReader();
      reader.onloadend = () => {
        resolve(Buffer.from(reader.result as ArrayBuffer));
      };
      reader.onerror = () => {
        reader.abort();
        reject(reader.error);
      };
      reader.readAsArrayBuffer(blob);
    });
  }

  static uniqueFileName(
    filePath: string,
    options?: UniqueFileNameOption
  ): Promise<string> {
    return new Promise<string>((resolve, reject) => {
      const dir = path.dirname(filePath);
      const ext = path.extname(filePath);
      const base = path.basename(filePath, ext);

      const uniqueFile: UniqueFile = {
        dir,
        ext,
        base
      };

      options = options || {};
      options.separator = options.separator || '-';
      options.mode = options.mode || 'numeric';

      if ('numeric' !== options.mode) {
        if (charsets[options.mode]) {
          options.charset = charsets[options.mode];
          options.mode = 'charset';
        } else if (
          'charset' !== options.mode ||
          ('charset' === options.mode && !options.charset)
        ) {
          options.mode = 'numeric';
        }
      }

      if (options.paddingSize && !options.paddingCharacter) {
        options.paddingCharacter = '0';
      }

      FileUtil.uniqueFileNameProcess(
        uniqueFile,
        options,
        (fileName: string) => {
          resolve(fileName);
        }
      );
    });
  }

  private static uniqueFileNameProcess(
    uniqueFile: UniqueFile,
    options: UniqueFileNameOption,
    callback: (fileName: string) => void
  ) {
    let fileName: string;
    let append = '';

    if (options.alwaysAppend && !uniqueFile.increment) {
      uniqueFile.increment = 1;
    }

    if (uniqueFile.increment) {
      if ('numeric' === options.mode) {
        append = '' + uniqueFile.increment;
      } else {
        append = FileUtil.numberToString(uniqueFile.increment, options.charset);
      }

      if (options.paddingSize) {
        while (append.length < options.paddingSize) {
          append = options.paddingCharacter + append;
        }
      }

      append = options.separator + append;
    }

    fileName = path.join(
      uniqueFile.dir,
      uniqueFile.base + append + uniqueFile.ext
    );
    if (fse.existsSync(fileName)) {
      if (uniqueFile.increment) {
        uniqueFile.increment += 1;
      } else {
        uniqueFile.increment = 'numeric' === options.mode ? 2 : 1;
      }
      return FileUtil.uniqueFileNameProcess(uniqueFile, options, callback);
    } else {
      return callback(fileName);
    }
  }

  private static numberToString(nbr: number, charset: string) {
    const charsetLen = charset.length;
    let strLen = 0;
    let strThisLen = 0;
    let tmp: number;

    for (let maxpower = 20; maxpower >= 0; maxpower--) {
      const maxvalue = FileUtil.sumOfPowerFromOne(charsetLen, maxpower);

      if (maxvalue < nbr) {
        strLen = maxpower + 1;
        strThisLen = maxvalue + Math.pow(charsetLen, maxpower + 1) - maxvalue;

        break;
      }
    }

    if (0 === strLen) {
      return null;
    }

    let str = '';
    while (--strLen >= 0) {
      if (strLen === 0) {
        str += charset.charAt(nbr - 1);
        break;
      }

      strThisLen = Math.pow(charsetLen, strLen);
      const initial = FileUtil.sumOfPowerFromOne(charsetLen, strLen - 1);

      for (tmp = charsetLen; tmp >= 1; tmp--) {
        if (initial + tmp * strThisLen < nbr) {
          break;
        }
      }

      nbr -= tmp * strThisLen;
      str += charset.charAt(tmp - 1);
    }

    return str;
  }

  private static sumOfPowerFromOne(base: number, maxpower: number) {
    let value = 0;
    for (let tmp = maxpower; tmp >= 1; tmp--) {
      value += Math.pow(base, tmp);
    }
    return value;
  }
}
