'use strict';

var normalize = require('value-or-function');

var slice = Array.prototype.slice;

function createResolver(config, options) {
  // TODO: should the config object be validated?
  config = config || {};
  options = options || {};

  var resolver = {
    resolve: resolve,
    resolveConstant: resolveConstant,
  };

  // Keep constants separately
  var constants = {};

  function resolveConstant(key) {
    if (Object.prototype.hasOwnProperty.call(constants, key)) {
      return constants[key];
    }

    var definition = config[key];
    // Ignore options that are not defined
    if (!definition) {
      return;
    }

    var option = options[key];

    if (option != null) {
      if (typeof option === 'function') {
        return;
      }
      option = normalize.call(resolver, definition.type, option);
      if (option != null) {
        constants[key] = option;
        return option;
      }
    }

    var fallback = definition.default;
    if (option == null && typeof fallback !== 'function') {
      constants[key] = fallback;
      return fallback;
    }
  }

  // Keep requested keys to detect (and disallow) recursive resolution
  var stack = [];

  function resolve(key) {
    var option = resolveConstant(key);
    if (option != null) {
      return option;
    }

    var definition = config[key];
    // Ignore options that are not defined
    if (!definition) {
      return;
    }

    if (stack.indexOf(key) >= 0) {
      throw new Error('Recursive resolution denied.');
    }

    option = options[key];
    var fallback = definition.default;
    var appliedArgs = slice.call(arguments, 1);
    var args = [definition.type, option].concat(appliedArgs);

    function toResolve() {
      stack.push(key);
      var option = normalize.apply(resolver, args);

      if (option == null) {
        option = fallback;
        if (typeof option === 'function') {
          option = option.apply(resolver, appliedArgs);
        }
      }

      return option;
    }

    function onResolve() {
      stack.pop();
    }

    return tryResolve(toResolve, onResolve);
  }

  return resolver;
}

function tryResolve(toResolve, onResolve) {
  try {
    return toResolve();
  } finally {
    onResolve();
  }
}

module.exports = createResolver;
