/*!
 * http-errors
 * Copyright(c) 2014 Jonathan Ong
 * Copyright(c) 2016 Douglas Christopher Wilson
 * MIT Licensed
 */

'use strict';

/**
 * Module dependencies.
 * @private
 */

var deprecate = require('depd')('http-errors');
var setPrototypeOf = require('setprototypeof');
var statuses = require('statuses');
var inherits = require('inherits');
var toIdentifier = require('toidentifier');

/**
 * Module exports.
 * @public
 */

module.exports = createError;
module.exports.HttpError = createHttpErrorConstructor();
module.exports.isHttpError = createIsHttpErrorFunction(
  module.exports.HttpError
);

// Populate exports for all constructors
populateConstructorExports(
  module.exports,
  statuses.codes,
  module.exports.HttpError
);

/**
 * Get the code class of a status code.
 * @private
 */

function codeClass(status) {
  return Number(String(status).charAt(0) + '00');
}

/**
 * Create a new HTTP Error.
 *
 * @returns {Error}
 * @public
 */

function createError() {
  // so much arity going on ~_~
  var err;
  var msg;
  var status = 500;
  var props = {};
  for (var i = 0; i < arguments.length; i++) {
    var arg = arguments[i];
    var type = typeof arg;
    if (type === 'object' && arg instanceof Error) {
      err = arg;
      status = err.status || err.statusCode || status;
    } else if (type === 'number' && i === 0) {
      status = arg;
    } else if (type === 'string') {
      msg = arg;
    } else if (type === 'object') {
      props = arg;
    } else {
      throw new TypeError('argument #' + (i + 1) + ' unsupported type ' + type);
    }
  }

  if (typeof status === 'number' && (status < 400 || status >= 600)) {
    deprecate('non-error status code; use only 4xx or 5xx status codes');
  }

  if (
    typeof status !== 'number' ||
    (!statuses.message[status] && (status < 400 || status >= 600))
  ) {
    status = 500;
  }

  // constructor
  var HttpError = createError[status] || createError[codeClass(status)];

  if (!err) {
    // create error
    err =
      HttpError ?
        new HttpError(msg)
      : new Error(msg || statuses.message[status]);
    Error.captureStackTrace(err, createError);
  }

  if (!HttpError || !(err instanceof HttpError) || err.status !== status) {
    // add properties to generic error
    err.expose = status < 500;
    err.status = err.statusCode = status;
  }

  for (var key in props) {
    if (key !== 'status' && key !== 'statusCode') {
      err[key] = props[key];
    }
  }

  return err;
}

/**
 * Create HTTP error abstract base class.
 * @private
 */

function createHttpErrorConstructor() {
  function HttpError() {
    throw new TypeError('cannot construct abstract class');
  }

  inherits(HttpError, Error);

  return HttpError;
}

/**
 * Create a constructor for a client error.
 * @private
 */

function createClientErrorConstructor(HttpError, name, code) {
  var className = toClassName(name);

  function ClientError(message) {
    // create the error object
    var msg = message != null ? message : statuses.message[code];
    var err = new Error(msg);

    // capture a stack trace to the construction point
    Error.captureStackTrace(err, ClientError);

    // adjust the [[Prototype]]
    setPrototypeOf(err, ClientError.prototype);

    // redefine the error message
    Object.defineProperty(err, 'message', {
      enumerable: true,
      configurable: true,
      value: msg,
      writable: true,
    });

    // redefine the error name
    Object.defineProperty(err, 'name', {
      enumerable: false,
      configurable: true,
      value: className,
      writable: true,
    });

    return err;
  }

  inherits(ClientError, HttpError);
  nameFunc(ClientError, className);

  ClientError.prototype.status = code;
  ClientError.prototype.statusCode = code;
  ClientError.prototype.expose = true;

  return ClientError;
}

/**
 * Create function to test is a value is a HttpError.
 * @private
 */

function createIsHttpErrorFunction(HttpError) {
  return function isHttpError(val) {
    if (!val || typeof val !== 'object') {
      return false;
    }

    if (val instanceof HttpError) {
      return true;
    }

    return (
      val instanceof Error &&
      typeof val.expose === 'boolean' &&
      typeof val.statusCode === 'number' &&
      val.status === val.statusCode
    );
  };
}

/**
 * Create a constructor for a server error.
 * @private
 */

function createServerErrorConstructor(HttpError, name, code) {
  var className = toClassName(name);

  function ServerError(message) {
    // create the error object
    var msg = message != null ? message : statuses.message[code];
    var err = new Error(msg);

    // capture a stack trace to the construction point
    Error.captureStackTrace(err, ServerError);

    // adjust the [[Prototype]]
    setPrototypeOf(err, ServerError.prototype);

    // redefine the error message
    Object.defineProperty(err, 'message', {
      enumerable: true,
      configurable: true,
      value: msg,
      writable: true,
    });

    // redefine the error name
    Object.defineProperty(err, 'name', {
      enumerable: false,
      configurable: true,
      value: className,
      writable: true,
    });

    return err;
  }

  inherits(ServerError, HttpError);
  nameFunc(ServerError, className);

  ServerError.prototype.status = code;
  ServerError.prototype.statusCode = code;
  ServerError.prototype.expose = false;

  return ServerError;
}

/**
 * Set the name of a function, if possible.
 * @private
 */

function nameFunc(func, name) {
  var desc = Object.getOwnPropertyDescriptor(func, 'name');

  if (desc && desc.configurable) {
    desc.value = name;
    Object.defineProperty(func, 'name', desc);
  }
}

/**
 * Populate the exports object with constructors for every error class.
 * @private
 */

function populateConstructorExports(exports, codes, HttpError) {
  codes.forEach(function forEachCode(code) {
    var CodeError;
    var name = toIdentifier(statuses.message[code]);

    switch (codeClass(code)) {
      case 400:
        CodeError = createClientErrorConstructor(HttpError, name, code);
        break;
      case 500:
        CodeError = createServerErrorConstructor(HttpError, name, code);
        break;
    }

    if (CodeError) {
      // export the constructor
      exports[code] = CodeError;
      exports[name] = CodeError;
    }
  });
}

/**
 * Get a class name from a name identifier.
 * @private
 */

function toClassName(name) {
  return name.substr(-5) !== 'Error' ? name + 'Error' : name;
}