var fs = require('fs');
var path = require('path');
var walk = require('walk').walk;

var async = require('./async');

function Instance(handlebars) {
  if (!(this instanceof Instance)) {
    return new Instance(handlebars);
  }

  // expose handlebars, allows users to use their versions
  // by overriding this early in their apps
  var self = this;

  self.handlebars = handlebars || require('handlebars').create();

  // cache for templates, express 3.x doesn't do this for us
  self.cache = {};

  self.__express = middleware.bind(this);

  // DEPRECATED, kept for backwards compatibility
  self.SafeString = this.handlebars.SafeString;
  self.Utils = this.handlebars.Utils;
};

// express 3.x template engine compliance
function middleware(filename, options, cb) {
  var self = this;
  var cache = self.cache;
  var handlebars = self.handlebars;

  self.async = async();

  // grab extension from filename
  // if we need a layout, we will look for one matching out extension
  var extension = path.extname(filename);

  // If passing the locals as data, create the handlebars options object now
  var handlebarsOpts = (self.__localsAsData) ? { data: options._locals } : undefined;

  // render the original file
  // cb(err, str)
  function render_file(locals, cb) {
    // cached?
    var template = cache[filename];
    if (template) {
      try {
        var res = template(locals, handlebarsOpts)
        self.async.done(function (values) {
          Object.keys(values).forEach(function (id) {
            res = res.replace(id, values[id])
          })

          cb(null, res)
        })
      } catch (err) {
        cb(prependFilenameToError(filename, err))
      }

      return
    }

    fs.readFile(filename, 'utf8', function(err, str){
      if (err) {
        return cb(err);
      }

      var template = handlebars.compile(str);
      if (locals.cache) {
        cache[filename] = template;
      }

      try {
        var res = template(locals, handlebarsOpts);
        self.async.done(function(values) {
          Object.keys(values).forEach(function(id) {
            res = res.replace(id, values[id]);
          });

          cb(null, res);
        });
      } catch (err) {
        cb(prependFilenameToError(filename, err))
      }
    });
  }

  // render with a layout
  function render_with_layout (filename, template, locals, cb) {
    render_file(locals, function(err, str) {
      if (err) {
        return cb(err);
      }

      locals.body = str;

      try {
        var res = template(locals, handlebarsOpts)
        self.async.done(function (values) {
          Object.keys(values).forEach(function (id) {
            res = res.replace(id, values[id])
          })

          cb(null, res)
        })
      } catch (err) {
        cb(prependFilenameToError(filename, err))
      }
    });
  }

  var layout = options.layout;

  // user did not specify a layout in the locals
  // check global layout state
  if (layout === undefined && options.settings && options.settings['view options']) {
    layout = options.settings['view options'].layout;
  }

  // user explicitly request no layout
  // either by specifying false for layout: false in locals
  // or by settings the false view options
  if (layout !== undefined && !layout) {
    return render_file(options, cb);
  }

  var view_dirs = options.settings.views;

  var layout_filename = [].concat(view_dirs).map(function (view_dir) {
    var view_path = path.join(view_dir, layout || 'layout');

    if (!path.extname(view_path)) {
      view_path += extension;
    }

    return view_path;
  });

  for (var i = 0; i < layout_filename.length; i++) {
    var layout_template = cache[layout_filename[i]]

    if (layout_template) {
      return render_with_layout(layout_filename[i], layout_template, options, cb)
    }
  }

  // TODO check if layout path has .hbs extension

  function prependFilenameToError (filename, err) {
    // prepend to the message
    if (typeof err.message === 'string') {
      err.message = filename + ': ' + err.message
    }

    return err
  }

  function cacheAndCompile(filename, str) {
    var layout_template = handlebars.compile(str);
    if (options.cache) {
      cache[filename] = layout_template;
    }

    render_with_layout(filename, layout_template, options, cb)
  }

  function tryReadFileAndCache(templates) {
    var template = templates.shift();

    fs.readFile(template, 'utf8', function(err, str) {
      if (err) {
        if (layout && templates.length === 0) {
          // Only return error if user explicitly asked for layout.
          return cb(err);
        }

        if (templates.length > 0) {
          return tryReadFileAndCache(templates);
        }

        return render_file(options, cb);
      }

      cacheAndCompile(template, str);
    });
  }

  tryReadFileAndCache(layout_filename);
}

// express 2.x template engine compliance
Instance.prototype.compile = function (str) {
  if (typeof str !== 'string') {
    return str;
  }

  var template = this.handlebars.compile(str);
  return function (locals) {
    return template(locals, {
      helpers: locals.blockHelpers,
      partials: null,
      data: null
    });
  };
};

Instance.prototype.registerHelper = function () {
  this.handlebars.registerHelper.apply(this.handlebars, arguments);
};

Instance.prototype.registerPartial = function () {
  this.handlebars.registerPartial.apply(this.handlebars, arguments);
};

Instance.prototype.registerPartials = function (directory, done) {
  var handlebars = this.handlebars;

  var register = function(filepath, done) {
    var isValidTemplate = /\.(html|hbs)$/.test(filepath);

    if (!isValidTemplate) {
      return done(null);
    }

    fs.readFile(filepath, 'utf8', function(err, data) {
      if (!err) {
        var ext = path.extname(filepath);
        var templateName = path.relative(directory, filepath)
          .slice(0, -(ext.length)).replace(/[ -]/g, '_')
          .replace(/\\/g, '/')
        handlebars.registerPartial(templateName, data);
      }

      done(err);
    });
  };

  walk(directory).on('file', function(root, stat, next) {
    register(path.join(root, stat.name), next);
  }).on('end', done || function() {});

};

Instance.prototype.registerAsyncHelper = function(name, fn) {
  var self = this;
  self.handlebars.registerHelper(name, function() {
    return self.async.resolve(fn, arguments);
  });
};

Instance.prototype.localsAsTemplateData = function(app) {
  // Set a flag to indicate we should pass locals as data
  this.__localsAsData = true;

  app.render = (function(render) {
    return function(view, options, callback) {
      if (typeof options === "function") {
        callback = options;
        options = {};
      }

      // Mix response.locals (options._locals) with app.locals (this.locals)
      options._locals = options._locals || {};
      for (var key in this.locals) {
        options._locals[key] = this.locals[key];
      }

      return render.call(this, view, options, callback);
    };
  })(app.render);
};

module.exports = new Instance();
module.exports.create = function(handlebars) {
  return new Instance(handlebars);
};
