/**
 * Copyright (c) 2015-present, Facebook, Inc.
 * All rights reserved.
 *
 * This source code is licensed under the BSD-style license found in the
 * LICENSE file in the root directory of this source tree. An additional grant
 * of patent rights can be found in the PATENTS file in the same directory.
 */
'use strict';

const Promise = require('promise');
const declareOpts = require('../lib/declareOpts');
const os = require('os');
const util = require('util');
const workerFarm = require('worker-farm');
const debug = require('debug')('ReactNativePackager:JStransformer');

// Avoid memory leaks caused in workers. This number seems to be a good enough number
// to avoid any memory leak while not slowing down initial builds.
// TODO(amasad): Once we get bundle splitting, we can drive this down a bit more.
const MAX_CALLS_PER_WORKER = 600;

// Worker will timeout if one of the callers timeout.
const DEFAULT_MAX_CALL_TIME = 301000;

// How may times can we tolerate failures from the worker.
const MAX_RETRIES = 2;

const validateOpts = declareOpts({
  transformModulePath: {
    type:'string',
    required: false,
  },
  transformTimeoutInterval: {
    type: 'number',
    default: DEFAULT_MAX_CALL_TIME,
  },
});

const maxConcurrentWorkers = ((cores, override) => {
  if (override) {
    return Math.min(cores, override);
  }

  if (cores < 3) {
    return cores;
  }
  if (cores < 8) {
    return Math.floor(cores * 0.75);
  }
  if (cores < 24) {
    return Math.floor(3/8 * cores + 3); // between cores *.75 and cores / 2
  }
  return cores / 2;
})(os.cpus().length, process.env.REACT_NATIVE_MAX_WORKERS);

class Transformer {
  constructor(options) {
    const opts = this._opts = validateOpts(options);

    const {transformModulePath} = opts;

    if (transformModulePath) {
      this._transformModulePath = require.resolve(transformModulePath);

      this._workers = workerFarm(
        {
          autoStart: true,
          maxConcurrentCallsPerWorker: 1,
          maxConcurrentWorkers: maxConcurrentWorkers,
          maxCallsPerWorker: MAX_CALLS_PER_WORKER,
          maxCallTime: opts.transformTimeoutInterval,
          maxRetries: MAX_RETRIES,
        },
        require.resolve('./worker'),
        ['minify', 'transformAndExtractDependencies']
      );

      this._transform = Promise.denodeify(this._workers.transformAndExtractDependencies);
      this.minify = Promise.denodeify(this._workers.minify);
    }
  }

  kill() {
    this._workers && workerFarm.end(this._workers);
  }

  transformFile(fileName, code, options) {
    if (!this._transform) {
      return Promise.reject(new Error('No transform module'));
    }
    debug('transforming file', fileName);
    return this
      ._transform(this._transformModulePath, fileName, code, options)
      .then(result => {
        debug('done transforming file', fileName);
        return result;
      })
      .catch(error => {
        if (error.type === 'TimeoutError') {
          const timeoutErr = new Error(
            `TimeoutError: transforming ${fileName} took longer than ` +
            `${this._opts.transformTimeoutInterval / 1000} seconds.\n` +
            `You can adjust timeout via the 'transformTimeoutInterval' option`
          );
          timeoutErr.type = 'TimeoutError';
          throw timeoutErr;
        } else if (error.type === 'ProcessTerminatedError') {
          const uncaughtError = new Error(
            'Uncaught error in the transformer worker: ' +
            this._opts.transformModulePath
          );
          uncaughtError.type = 'ProcessTerminatedError';
          throw uncaughtError;
        }

        throw formatError(error, fileName);
      });
  }
}

module.exports = Transformer;

Transformer.TransformError = TransformError;

function TransformError() {
  Error.captureStackTrace && Error.captureStackTrace(this, TransformError);
}
util.inherits(TransformError, SyntaxError);

function formatError(err, filename, source) {
  if (err.loc) {
    return formatBabelError(err, filename, source);
  } else {
    return formatGenericError(err, filename, source);
  }
}

function formatGenericError(err, filename) {
  var msg = 'TransformError: ' + filename + ': ' + err.message;
  var error = new TransformError();
  var stack = (err.stack || '').split('\n').slice(0, -1);
  stack.push(msg);
  error.stack = stack.join('\n');
  error.message = msg;
  error.type = 'TransformError';
  return error;
}

function formatBabelError(err, filename) {
  var error = new TransformError();
  error.type = 'TransformError';
  error.message = (err.type || error.type) + ' ' + err.message;
  error.stack = err.stack;
  error.snippet = err.codeFrame;
  error.lineNumber = err.loc.line;
  error.column = err.loc.column;
  error.filename = filename;
  error.description = err.message;
  return error;
}
