/*!
 * Express - HTTPServer
 * Copyright(c) 2010 TJ Holowaychuk <tj@vision-media.ca>
 * MIT Licensed
 */

/**
 * Module dependencies.
 */

var qs = require('qs')
  , connect = require('connect')
  , router = require('./router')
  , Router = require('./router')
  , view = require('./view')
  , toArray = require('./utils').toArray
  , methods = router.methods.concat('del', 'all')
  , url = require('url')
  , utils = connect.utils;

/**
 * Expose `HTTPServer`.
 */

exports = module.exports = HTTPServer;

/**
 * Server proto.
 */

var app = HTTPServer.prototype;

/**
 * Initialize a new `HTTPServer` with optional `middleware`.
 *
 * @param {Array} middleware
 * @api public
 */

function HTTPServer(middleware){
  connect.HTTPServer.call(this, []);
  this.init(middleware);
};

/**
 * Inherit from `connect.HTTPServer`.
 */

app.__proto__ = connect.HTTPServer.prototype;

/**
 * Initialize the server.
 *
 * @param {Array} middleware
 * @api private
 */

app.init = function(middleware){
  var self = this;
  this.cache = {};
  this.settings = {};
  this.redirects = {};
  this.isCallbacks = {};
  this._locals = {};
  this.dynamicViewHelpers = {};
  this.errorHandlers = [];

  this.set('env', process.env.NODE_ENV || 'development');

  // expose objects to each other
  this.use(function(req, res, next){
    req.query = req.query || {};
    res.setHeader('X-Powered-By', 'Express');
    req.app = res.app = self;
    req.res = res;
    res.req = req;
    req.next = next;
    // assign req.query
    if (req.url.indexOf('?') > 0) {
      var query = url.parse(req.url).query;
      req.query = qs.parse(query);
    }
    next();
  });

  // apply middleware
  if (middleware) middleware.forEach(self.use.bind(self));

  // router
  this.routes = new Router(this);
  this.__defineGetter__('router', function(){
    this.__usedRouter = true;
    return self.routes.middleware;
  });

  // default locals
  this.locals({
      settings: this.settings
    , app: this
  });

  // default development configuration
  this.configure('development', function(){
    this.enable('hints');
  });

  // default production configuration
  this.configure('production', function(){
    this.enable('view cache');
  });

  // register error handlers on "listening"
  // so that they disregard definition position.
  this.on('listening', this.registerErrorHandlers.bind(this));

  // route manipulation methods
  methods.forEach(function(method){
    self.lookup[method] = function(path){
      return self.routes.lookup(method, path);
    };

    self.match[method] = function(path){
      return self.routes.match(method, path);
    };

    self.remove[method] = function(path){
      return self.routes.lookup(method, path).remove();
    };
  });

  // del -> delete
  self.lookup.del = self.lookup.delete;
  self.match.del = self.match.delete;
  self.remove.del = self.remove.delete;
};

/**
 * Remove routes matching the given `path`.
 *
 * @param {Route} path
 * @return {Boolean}
 * @api public
 */

app.remove = function(path){
  return this.routes.lookup('all', path).remove();
};

/**
 * Lookup routes defined with a path
 * equivalent to `path`.
 *
 * @param {Stirng} path
 * @return {Array}
 * @api public
 */

app.lookup = function(path){
  return this.routes.lookup('all', path);
};

/**
 * Lookup routes matching the given `url`.
 *
 * @param {Stirng} url
 * @return {Array}
 * @api public
 */

app.match = function(url){
  return this.routes.match('all', url);
};

/**
 * When using the vhost() middleware register error handlers.
 */

app.onvhost = function(){
  this.registerErrorHandlers();
};

/**
 * Register error handlers.
 *
 * @return {Server} for chaining
 * @api public
 */

app.registerErrorHandlers = function(){
  this.errorHandlers.forEach(function(fn){
    this.use(function(err, req, res, next){
      fn.apply(this, arguments);
    });
  }, this);
  return this;
};

/**
 * Proxy `connect.HTTPServer#use()` to apply settings to
 * mounted applications.
 *
 * @param {String|Function|Server} route
 * @param {Function|Server} middleware
 * @return {Server} for chaining
 * @api public
 */

app.use = function(route, middleware){
  var app, base, handle;

  if ('string' != typeof route) {
    middleware = route, route = '/';
  }

  // express app
  if (middleware.handle && middleware.set) app = middleware;

  // restore .app property on req and res
  if (app) {
    app.route = route;
    middleware = function(req, res, next) {
      var orig = req.app;
      app.handle(req, res, function(err){
        req.app = res.app = orig;
        next(err);
      });
    };
  }

  connect.HTTPServer.prototype.use.call(this, route, middleware);

  // mounted an app, invoke the hook
  // and adjust some settings
  if (app) {
    base = this.set('basepath') || this.route;
    if ('/' == base) base = '';
    base = base + (app.set('basepath') || app.route);
    app.set('basepath', base);
    app.parent = this;
    if (app.__mounted) app.__mounted.call(app, this);
  }

  return this;
};

/**
 * Assign a callback `fn` which is called
 * when this `Server` is passed to `Server#use()`.
 *
 * Examples:
 *
 *    var app = express.createServer()
 *      , blog = express.createServer();
 *
 *    blog.mounted(function(parent){
 *      // parent is app
 *      // "this" is blog
 *    });
 *
 *    app.use(blog);
 *
 * @param {Function} fn
 * @return {Server} for chaining
 * @api public
 */

app.mounted = function(fn){
  this.__mounted = fn;
  return this;
};

/**
 * See: view.register.
 *
 * @return {Server} for chaining
 * @api public
 */

app.register = function(){
  view.register.apply(this, arguments);
  return this;
};

/**
 * Register the given view helpers `obj`. This method
 * can be called several times to apply additional helpers.
 *
 * @param {Object} obj
 * @return {Server} for chaining
 * @api public
 */

app.helpers =
app.locals = function(obj){
  utils.merge(this._locals, obj);
  return this;
};

/**
 * Register the given dynamic view helpers `obj`. This method
 * can be called several times to apply additional helpers.
 *
 * @param {Object} obj
 * @return {Server} for chaining
 * @api public
 */

app.dynamicHelpers = function(obj){
  utils.merge(this.dynamicViewHelpers, obj);
  return this;
};

/**
 * Map the given param placeholder `name`(s) to the given callback(s).
 *
 * Param mapping is used to provide pre-conditions to routes
 * which us normalized placeholders. This callback has the same
 * signature as regular middleware, for example below when ":userId"
 * is used this function will be invoked in an attempt to load the user.
 *
 *      app.param('userId', function(req, res, next, id){
 *        User.find(id, function(err, user){
 *          if (err) {
 *            next(err);
 *          } else if (user) {
 *            req.user = user;
 *            next();
 *          } else {
 *            next(new Error('failed to load user'));
 *          }
 *        });
 *      });
 *
 * Passing a single function allows you to map logic
 * to the values passed to `app.param()`, for example
 * this is useful to provide coercion support in a concise manner.
 *
 * The following example maps regular expressions to param values
 * ensuring that they match, otherwise passing control to the next
 * route:
 *
 *      app.param(function(name, regexp){
 *        if (regexp instanceof RegExp) {
 *          return function(req, res, next, val){
 *            var captures;
 *            if (captures = regexp.exec(String(val))) {
 *              req.params[name] = captures;
 *              next();
 *            } else {
 *              next('route');
 *            }
 *          }
 *        }
 *      });
 *
 * We can now use it as shown below, where "/commit/:commit" expects
 * that the value for ":commit" is at 5 or more digits. The capture
 * groups are then available as `req.params.commit` as we defined
 * in the function above.
 *
 *    app.param('commit', /^\d{5,}$/);
 *
 * For more of this useful functionality take a look
 * at [express-params](http://github.com/visionmedia/express-params).
 *
 * @param {String|Array|Function} name
 * @param {Function} fn
 * @return {Server} for chaining
 * @api public
 */

app.param = function(name, fn){
  var self = this
    , fns = [].slice.call(arguments, 1);

  // array
  if (Array.isArray(name)) {
    name.forEach(function(name){
      fns.forEach(function(fn){
        self.param(name, fn);
      });
    });
  // param logic
  } else if ('function' == typeof name) {
    this.routes.param(name);
  // single
  } else {
    if (':' == name[0]) name = name.substr(1);
    fns.forEach(function(fn){
      self.routes.param(name, fn);
    });
  }

  return this;
};

/**
 * Assign a custom exception handler callback `fn`.
 * These handlers are always _last_ in the middleware stack.
 *
 * @param {Function} fn
 * @return {Server} for chaining
 * @api public
 */

app.error = function(fn){
  this.errorHandlers.push(fn);
  return this;
};

/**
 * Register the given callback `fn` for the given `type`.
 *
 * @param {String} type
 * @param {Function} fn
 * @return {Server} for chaining
 * @api public
 */

app.is = function(type, fn){
  if (!fn) return this.isCallbacks[type];
  this.isCallbacks[type] = fn;
  return this;
};

/**
 * Assign `setting` to `val`, or return `setting`'s value.
 * Mounted servers inherit their parent server's settings.
 *
 * @param {String} setting
 * @param {String} val
 * @return {Server|Mixed} for chaining, or the setting value
 * @api public
 */

app.set = function(setting, val){
  if (val === undefined) {
    if (this.settings.hasOwnProperty(setting)) {
      return this.settings[setting];
    } else if (this.parent) {
      return this.parent.set(setting);
    }
  } else {
    this.settings[setting] = val;
    return this;
  }
};

/**
 * Check if `setting` is enabled.
 *
 * @param {String} setting
 * @return {Boolean}
 * @api public
 */

app.enabled = function(setting){
  return !!this.set(setting);
};

/**
 * Check if `setting` is disabled.
 *
 * @param {String} setting
 * @return {Boolean}
 * @api public
 */

app.disabled = function(setting){
  return !this.set(setting);
};

/**
 * Enable `setting`.
 *
 * @param {String} setting
 * @return {Server} for chaining
 * @api public
 */

app.enable = function(setting){
  return this.set(setting, true);
};

/**
 * Disable `setting`.
 *
 * @param {String} setting
 * @return {Server} for chaining
 * @api public
 */

app.disable = function(setting){
  return this.set(setting, false);
};

/**
 * Redirect `key` to `url`.
 *
 * @param {String} key
 * @param {String} url
 * @return {Server} for chaining
 * @api public
 */

app.redirect = function(key, url){
  this.redirects[key] = url;
  return this;
};

/**
 * Configure callback for zero or more envs,
 * when no env is specified that callback will
 * be invoked for all environments. Any combination
 * can be used multiple times, in any order desired.
 *
 * Examples:
 *
 *    app.configure(function(){
 *      // executed for all envs
 *    });
 *
 *    app.configure('stage', function(){
 *      // executed staging env
 *    });
 *
 *    app.configure('stage', 'production', function(){
 *      // executed for stage and production
 *    });
 *
 * @param {String} env...
 * @param {Function} fn
 * @return {Server} for chaining
 * @api public
 */

app.configure = function(env, fn){
  var envs = 'all'
    , args = toArray(arguments);
  fn = args.pop();
  if (args.length) envs = args;
  if ('all' == envs || ~envs.indexOf(this.settings.env)) fn.call(this);
  return this;
};

/**
 * Delegate `.VERB(...)` calls to `.route(VERB, ...)`.
 */

methods.forEach(function(method){
  app[method] = function(path){
    if (1 == arguments.length) return this.routes.lookup(method, path);
    var args = [method].concat(toArray(arguments));
    if (!this.__usedRouter) this.use(this.router);
    return this.routes._route.apply(this.routes, args);
  }
});

/**
 * Special-cased "all" method, applying the given route `path`,
 * middleware, and callback to _every_ HTTP method.
 *
 * @param {String} path
 * @param {Function} ...
 * @return {Server} for chaining
 * @api public
 */

app.all = function(path){
  var args = arguments;
  if (1 == args.length) return this.routes.lookup('all', path);
  methods.forEach(function(method){
    if ('all' == method || 'del' == method) return;
    app[method].apply(this, args);
  }, this);
  return this;
};

// del -> delete alias

app.del = app.delete;

