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

/**
 * Module dependencies.
 */

var path = require('path')
  , extname = path.extname
  , dirname = path.dirname
  , basename = path.basename
  , utils = require('connect').utils
  , View = require('./view/view')
  , partial = require('./view/partial')
  , union = require('./utils').union
  , merge = utils.merge
  , http = require('http')
  , res = http.ServerResponse.prototype;

/**
 * Expose constructors.
 */

exports = module.exports = View;

/**
 * Export template engine registrar.
 */

exports.register = View.register;

/**
 * Lookup and compile `view` with cache support by supplying
 * both the `cache` object and `cid` string,
 * followed by `options` passed to `exports.lookup()`.
 *
 * @param {String} view
 * @param {Object} cache
 * @param {Object} cid
 * @param {Object} options
 * @return {View}
 * @api private
 */

exports.compile = function(view, cache, cid, options){
  if (cache && cid && cache[cid]){
    options.filename = cache[cid].path;
    return cache[cid];
  }

  // lookup
  view = exports.lookup(view, options);

  // hints
  if (!view.exists) {
    if (options.hint) hintAtViewPaths(view.original, options);
    var err = new Error('failed to locate view "' + view.original.view + '"');
    err.view = view.original;
    throw err;
  }

  // compile
  options.filename = view.path;
  view.fn = view.templateEngine.compile(view.contents, options);
  cache[cid] = view;

  return view;
};

/**
 * Lookup `view`, returning an instanceof `View`.
 *
 * Options:
 *
 *   - `root` root directory path
 *   - `defaultEngine` default template engine
 *   - `parentView` parent `View` object
 *   - `cache` cache object
 *   - `cacheid` optional cache id
 *
 * Lookup:
 *
 *   - partial `_<name>`
 *   - any `<name>/index`
 *   - non-layout `../<name>/index`
 *   - any `<root>/<name>`
 *   - partial `<root>/_<name>`
 *
 * @param {String} view
 * @param {Object} options
 * @return {View}
 * @api private
 */

exports.lookup = function(view, options){
  var orig = view = new View(view, options)
    , partial = options.isPartial
    , layout = options.isLayout;

  // Try _ prefix ex: ./views/_<name>.jade
  // taking precedence over the direct path
  if (partial) {
    view = new View(orig.prefixPath, options);
    if (!view.exists) view = orig;
  }

  // Try index ex: ./views/user/index.jade
  if (!layout && !view.exists) view = new View(orig.indexPath, options);

  // Try ../<name>/index ex: ../user/index.jade
  // when calling partial('user') within the same dir
  if (!layout && !view.exists) view = new View(orig.upIndexPath, options);

  // Try root ex: <root>/user.jade
  if (!view.exists) view = new View(orig.rootPath, options);

  // Try root _ prefix ex: <root>/_user.jade
  if (!view.exists && partial) view = new View(view.prefixPath, options);

  view.original = orig;
  return view;
};

/**
 * Partial render helper.
 *
 * @api private
 */

function renderPartial(res, view, options, parentLocals, parent){
  var collection, object, locals;

  if (options) {
    // collection
    if (options.collection) {
      collection = options.collection;
      delete options.collection;
    } else if ('length' in options) {
      collection = options;
      options = {};
    }

    // locals
    if (options.locals) {
      locals = options.locals;
      delete options.locals;
    }

    // object
    if ('Object' != options.constructor.name) {
      object = options;
      options = {};
    } else if (undefined != options.object) {
      object = options.object;
      delete options.object;
    }
  } else {
    options = {};
  }

  // Inherit locals from parent
  union(options, parentLocals);

  // Merge locals
  if (locals) merge(options, locals);

  // Partials dont need layouts
  options.isPartial = true;
  options.layout = false;

  // Deduce name from view path
  var name = options.as || partial.resolveObjectName(view);

  // Render partial
  function render(){
    if (object) {
      if ('string' == typeof name) {
        options[name] = object;
      } else if (name === global) {
        merge(options, object);
      }
    }
    return res.render(view, options, null, parent, true);
  }

  // Collection support
  if (collection) {
    var len = collection.length
      , buf = ''
      , keys
      , key
      , val;

    options.collectionLength = len;

    if ('number' == typeof len || Array.isArray(collection)) {
      for (var i = 0; i < len; ++i) {
        val = collection[i];
        options.firstInCollection = i == 0;
        options.indexInCollection = i;
        options.lastInCollection = i == len - 1;
        object = val;
        buf += render();
      }
    } else {
      keys = Object.keys(collection);
      len = keys.length;
      options.collectionLength = len;
      options.collectionKeys = keys;
      for (var i = 0; i < len; ++i) {
        key = keys[i];
        val = collection[key];
        options.keyInCollection = key;
        options.firstInCollection = i == 0;
        options.indexInCollection = i;
        options.lastInCollection = i == len - 1;
        object = val;
        buf += render();
      }
    }

    return buf;
  } else {
    return render();
  }
};

/**
 * Render `view` partial with the given `options`. Optionally a
 * callback `fn(err, str)` may be passed instead of writing to
 * the socket.
 *
 * Options:
 *
 *   - `object` Single object with name derived from the view (unless `as` is present)
 *
 *   - `as` Variable name for each `collection` value, defaults to the view name.
 *     * as: 'something' will add the `something` local variable
 *     * as: this will use the collection value as the template context
 *     * as: global will merge the collection value's properties with `locals`
 *
 *   - `collection` Array of objects, the name is derived from the view name itself.
 *     For example _video.html_ will have a object _video_ available to it.
 *
 * @param  {String} view
 * @param  {Object|Array|Function} options, collection, callback, or object
 * @param  {Function} fn
 * @return {String}
 * @api public
 */

res.partial = function(view, options, fn){
  var app = this.app
    , options = options || {}
    , viewEngine = app.set('view engine')
    , parent = {};

  // accept callback as second argument
  if ('function' == typeof options) {
    fn = options;
    options = {};
  }

  // root "views" option
  parent.dirname = app.set('views') || process.cwd() + '/views';

  // utilize "view engine" option
  if (viewEngine) parent.engine = viewEngine;

  // render the partial
  try {
    var str = renderPartial(this, view, options, null, parent);
  } catch (err) {
    if (fn) {
      fn(err);
    } else {
      this.req.next(err);
    }
    return;
  }

  // callback or transfer
  if (fn) {
    fn(null, str);
  } else {
    this.send(str);
  }
};

/**
 * Render `view` with the given `options` and optional callback `fn`.
 * When a callback function is given a response will _not_ be made
 * automatically, however otherwise a response of _200_ and _text/html_ is given.
 *
 * Options:
 *
 *  - `scope`     Template evaluation context (the value of `this`)
 *  - `debug`     Output debugging information
 *  - `status`    Response status code
 *
 * @param  {String} view
 * @param  {Object|Function} options or callback function
 * @param  {Function} fn
 * @api public
 */

res.render = function(view, opts, fn, parent, sub){
  // support callback function as second arg
  if ('function' == typeof opts) {
    fn = opts, opts = null;
  }

  try {
    return this._render(view, opts, fn, parent, sub);
  } catch (err) {
    // callback given
    if (fn) {
      fn(err);
    // unwind to root call to prevent multiple callbacks
    } else if (sub) {
      throw err;
    // root template, next(err)
    } else {
      this.req.next(err);
    }
  }
};

// private render()

res._render = function(view, opts, fn, parent, sub){
  var options = {}
    , self = this
    , app = this.app
    , helpers = app._locals
    , dynamicHelpers = app.dynamicViewHelpers
    , viewOptions = app.set('view options')
    , root = app.set('views') || process.cwd() + '/views';

  // cache id
  var cid = app.enabled('view cache')
    ? view + (parent ? ':' + parent.path : '')
    : false;

  // merge "view options"
  if (viewOptions) merge(options, viewOptions);

  // merge res._locals
  if (this._locals) merge(options, this._locals);

  // merge render() options
  if (opts) merge(options, opts);

  // merge render() .locals
  if (opts && opts.locals) merge(options, opts.locals);

  // status support
  if (options.status) this.statusCode = options.status;

  // capture attempts
  options.attempts = [];

  var partial = options.isPartial
    , layout = options.layout;

  // Layout support
  if (true === layout || undefined === layout) {
    layout = 'layout';
  }

  // Default execution scope to a plain object
  options.scope = options.scope || {};

  // Populate view
  options.parentView = parent;

  // "views" setting
  options.root = root;

  // "view engine" setting
  options.defaultEngine = app.set('view engine');

  // charset option
  if (options.charset) this.charset = options.charset;

  // Dynamic helper support
  if (false !== options.dynamicHelpers) {
    // cache
    if (!this.__dynamicHelpers) {
      this.__dynamicHelpers = {};
      for (var key in dynamicHelpers) {
        this.__dynamicHelpers[key] = dynamicHelpers[key].call(
            this.app
          , this.req
          , this);
      }
    }

    // apply
    merge(options, this.__dynamicHelpers);
  }

  // Merge view helpers
  union(options, helpers);

  // Always expose partial() as a local
  options.partial = function(path, opts){
    return renderPartial(self, path, opts, options, view);
  };

  // View lookup
  options.hint = app.enabled('hints');
  view = exports.compile(view, app.cache, cid, options);

  // layout helper
  options.layout = function(path){
    layout = path;
  };

  // render
  var str = view.fn.call(options.scope, options);

  // layout expected
  if (layout) {
    options.isLayout = true;
    options.layout = false;
    options.body = str;
    this.render(layout, options, fn, view, true);
  // partial return
  } else if (partial) {
    return str;
  // render complete, and
  // callback given
  } else if (fn) {
    fn(null, str);
  // respond
  } else {
    this.send(str);
  }
}

/**
 * Hint at view path resolution, outputting the
 * paths that Express has tried.
 *
 * @api private
 */

function hintAtViewPaths(view, options) {
  console.error();
  console.error('failed to locate view "' + view.view + '", tried:');
  options.attempts.forEach(function(path){
    console.error('  - %s', path);
  });
  console.error();
}
