text: 'use strict';
  
  const fs = require('fs');
  const path = require('path');
  const Readable = require('stream').Readable;
  
  // Parameters for safe file name parsing.
  const SAFE_FILE_NAME_REGEX = /[^\w-]/g;
  const MAX_EXTENSION_LENGTH = 3;
  
  // Parameters which used to generate unique temporary file names:
  const TEMP_COUNTER_MAX = 65536;
  const TEMP_PREFIX = 'tmp';
  let tempCounter = 0;
  
  /**
   * Logs message to console if debug option set to true.
   * @param {Object} options - options object.
   * @param {String} msg     - message to log.
   * @returns {Boolean}
   */
  const debugLog = (options, msg) => {
    options = options || {};
    if (!options.debug) return false;
    console.log(msg); // eslint-disable-line
    return true;
  };
  
  /**
   * Generates unique temporary file name like: tmp-5000-156788789789.
   * @param prefix {String} - a prefix for generated unique file name.
   * @returns {String}
   */
  const getTempFilename = (prefix) => {
    prefix = prefix || TEMP_PREFIX;
    tempCounter ++;
    if (tempCounter > TEMP_COUNTER_MAX) tempCounter = 1;
    return `${prefix}-${tempCounter}-${Date.now()}`;
  };
  
  /**
   * Returns true if argument is a function.
   * @returns {Boolean}
   */
  const isFunc = func => func && func.constructor && func.call && func.apply ? true: false;
  
  /**
   * Set errorFunc to the same value as successFunc for callback mode.
   * @returns {Function}
   */
  const errorFunc = (resolve, reject) => isFunc(reject) ? reject : resolve;
  
  /**
   * Return a callback function for promise resole/reject args.
   * @returns {Function}
   */
  const promiseCallback = (resolve, reject) => {
    return (err) => {
      if (err) {
        errorFunc(resolve, reject)(err);
      } else {
        resolve();
      }
    };
  };
  
  /**
   * Builds instance options from arguments objects.
   * @returns {Object} - result options.
   */
  const buildOptions = function(){
    let result = {};
    [...arguments].forEach(options => {
      if (!options || typeof options !== 'object') return;
      Object.keys(options).forEach(key => result[key] = options[key]);
    });
    return result;
  };
  
  /**
   * Builds request fields (using to build req.body and req.files)
   * @param {Object} instance - request object.
   * @param {String} field    - field name.
   * @param value             - field value.
   * @returns {Object}
   */
  const buildFields = (instance, field, value) => {
    //Do nothing if value is not set.
    if (value === null || value === undefined){
      return instance;
    }
    instance = instance || {};
    // Non-array fields
    if (!instance[field]) {
      instance[field] = value;
    } else {
    // Array fields  
      if (instance[field] instanceof Array){
        instance[field].push(value);
      } else {
        instance[field] = [instance[field], value];
      }
    }
    return instance;
  };
  
  /**
   * Creates a folder for file specified in the path variable
   * @param {Object} fileUploadOptions
   * @param {String} filePath
   */
  const checkAndMakeDir = function(fileUploadOptions, filePath){
    //Check upload options were set.
    if (!fileUploadOptions) return false;
    if (!fileUploadOptions.createParentPath) return false;
    //Check whether folder for the file exists.
    if (!filePath) return false;
    const parentPath = path.dirname(filePath);
    //Create folder if it is not exists.
    if (!fs.existsSync(parentPath)) fs.mkdirSync(parentPath); 
    return true;
  };
  
  /**
   * Delete file.
   * @param {String} file - Path to the file to delete.
   */
  const deleteFile = (file, callback) => {
    fs.unlink(file, (err) => {
      if (err) {
        callback(err);
        return;
      }
      callback();
    });  
  };
  
  /**
   * Copy file via streams
   * @param {String} src - Path to the source file
   * @param {String} dst - Path to the destination file.
   */
  const copyFile = function(src, dst, callback){
    //cbCalled flag and runCb helps to run cb only once.
    let cbCalled = false;
    let runCb = (err) => {
      if (cbCalled) return;
      cbCalled = true;
      callback(err);
    };
    //Create read stream
    let readable = fs.createReadStream(src);
    readable.on('error', runCb);
    //Create write stream
    let writable = fs.createWriteStream(dst);
    writable.on('error', (err)=>{
      readable.destroy();
      runCb(err);
    });
    writable.on('close', () => runCb());
    //Copy file via piping streams.
    readable.pipe(writable);
  };
  
  /**
   * Move file via streams by copieng and the deleteing src.
   * @param {String}   src      - Path to the source file
   * @param {String}   dst      - Path to the destination file.
   * @param {Function} callback - A callback function.
   */
  const moveFile = (src, dst, callback) => {
    // Copy file to dst.
    copyFile(src, dst, (err) => {
      if (err) {
        callback(err);
        return;
      }
      // Delete src file.
      deleteFile(src, callback);
    });
  };
  
  /**
   * Save buffer data to a file.
   * @param {Buffer} buffer - buffer to save to a file.
   * @param {String} filePath - path to a file.
   */
  const saveBufferToFile = function(buffer, filePath, callback){
    if (!Buffer.isBuffer(buffer)){
      callback(new Error('buffer variable should be a Buffer!'));
      return;
    }
    //Setup readable stream from buffer.
    let streamData = buffer;
    let readStream = Readable();
    readStream._read = ()=>{
      readStream.push(streamData);
      streamData = null;
    };
    //Setup file system writable stream.
    let fstream = fs.createWriteStream(filePath);
    fstream.on('error', error => callback(error));
    fstream.on('close', () => callback());
    //Copy file via piping streams.
    readStream.pipe(fstream);
  };
  
  /**
   * Parses filename and extension and returns object {name, extension}.
   * @param preserveExtension {Boolean, Integer} - true/false or number of characters for extension.
   * @param fileName {String}                    - file name to parse.
   * @returns {Object}                           - {name, extension}.
   */
  const parseFileNameExtension = (preserveExtension, fileName) => {
    let preserveExtensionLengh = parseInt(preserveExtension);
    let result = {name: fileName, extension: ''};
    if (!preserveExtension && preserveExtensionLengh !== 0){
      return result;
    }
    // Define maximum extension length
    let maxExtLength = isNaN(preserveExtensionLengh)
      ? MAX_EXTENSION_LENGTH
      : Math.abs(preserveExtensionLengh);
  
    let nameParts = fileName.split('.');
    if (nameParts.length < 2) {
      return result;
    }
    
    let extension = nameParts.pop();
    if (
      extension.length > maxExtLength &&
      maxExtLength > 0
    ) {
      nameParts[nameParts.length - 1] +=
        '.' +
        extension.substr(0, extension.length - maxExtLength);
      extension = extension.substr(-maxExtLength);
    }
  
    result.extension = maxExtLength ? extension : '';
    result.name = nameParts.join('.');
    return result;
  };
  
  /**
   * Parse file name and extension.
   * @param opts {Object}     - middleware options.
   * @param fileName {String} - Uploaded file name.
   * @returns {String}
   */
  const parseFileName = (opts, fileName) => {
    if (!opts.safeFileNames) {
      return fileName;
    }
    // Set regular expression for the file name.
    let safeNameRegex = typeof opts.safeFileNames === 'object' && opts.safeFileNames instanceof RegExp
      ? opts.safeFileNames
      : SAFE_FILE_NAME_REGEX;
    // Parse file name extension.
    let {name, extension} = parseFileNameExtension(opts.preserveExtension, fileName);
    if (extension.length) extension = '.' + extension.replace(safeNameRegex, '');
  
    return name.replace(safeNameRegex, '').concat(extension);
  };
  
  module.exports = {
    debugLog,
    isFunc,
    errorFunc,
    promiseCallback,
    buildOptions,
    buildFields,
    checkAndMakeDir,
    deleteFile,         // For testing purpose.
    copyFile,           // For testing purpose.
    moveFile,
    saveBufferToFile,
    parseFileName,
    getTempFilename
  };
  