/*!
 * Module dependencies.
 */

var DataSource = require('loopback-datasource-juggler').DataSource
  , ModelBuilder = require('loopback-datasource-juggler').ModelBuilder
  , compat = require('./compat')
  , assert = require('assert')
  , fs = require('fs')
  , RemoteObjects = require('strong-remoting')
  , swagger = require('strong-remoting/ext/swagger')
  , stringUtils = require('underscore.string')
  , path = require('path');

/**
 * The `App` object represents a Loopback application.
 * 
 * The App object extends [Express](http://expressjs.com/api.html#express) and
 * supports
 * [Express / Connect middleware](http://expressjs.com/api.html#middleware). See
 * [Express documentation](http://expressjs.com/api.html) for details.
 * 
 * ```js
 * var loopback = require('loopback');
 * var app = loopback();
 * 
 * app.get('/', function(req, res){
 *   res.send('hello world');
 * });
 * 
 * app.listen(3000);
 * ```
 * 
 * @class LoopBackApplication
 * @header var app = loopback()
 */
function App() {
  // this is a dummy placeholder for jsdox
}

/*!
 * Export the app prototype.
 */

var app = exports = module.exports = {};

/**
 * Lazily load a set of [remote objects](http://apidocs.strongloop.com/strong-remoting/#remoteobjectsoptions).
 *
 * **NOTE:** Calling `app.remotes()` multiple times will only ever return a
 * single set of remote objects.
 * @returns {RemoteObjects}
 */

app.remotes = function () {
  if(this._remotes) {
    return this._remotes;
  } else {
    return (this._remotes = RemoteObjects.create());
  }
}

/*!
 * Remove a route by reference.
 */

app.disuse = function (route) {
  if(this.stack) {
    for (var i = 0; i < this.stack.length; i++) {
      if(this.stack[i].route === route) {
        this.stack.splice(i, 1);
      }
    }
  }
}

/**
 * Define and attach a model to the app. The `Model` will be available on the
 * `app.models` object.
 *
 * ```js
 * var Widget = app.model('Widget', {dataSource: 'db'});
 * Widget.create({name: 'pencil'});
 * app.models.Widget.find(function(err, widgets) {
 *   console.log(widgets[0]); // => {name: 'pencil'}
 * });
 * ```
 * 
 * @param {String} modelName The name of the model to define
 * @options {Object} config The model's configuration
 * @property {String} dataSource The `DataSource` to attach the model to
 * @property {Object} [options] an object containing `Model` options
 * @property {Object} [properties] object defining the `Model` properties in [LoopBack Definition Language](http://docs.strongloop.com/loopback-datasource-juggler/#loopback-definition-language)
 * @end
 * @returns {ModelConstructor} the model class
 */

app.model = function (Model, config) {
  if(arguments.length === 1) {
    assert(typeof Model === 'function', 'app.model(Model) => Model must be a function / constructor');
    assert(Model.modelName, 'Model must have a "modelName" property');
    var remotingClassName = compat.getClassNameForRemoting(Model);
    this.remotes().exports[remotingClassName] = Model;
    this.models().push(Model);
    Model.shared = true;
    Model.app = this;
    Model.emit('attached', this);
    return;
  }
  var modelName = Model;
  config = config || {};
  assert(typeof modelName === 'string', 'app.model(name, config) => "name" name must be a string');

  Model =
  this.models[modelName] =
  this.models[classify(modelName)] =
  this.models[camelize(modelName)] = modelFromConfig(modelName, config, this);

  if(config.public !== false) {
    this.model(Model);
  }

  return Model;
}

/**
 * Get the models exported by the app. Only models defined using `app.model()`
 * will show up in this list.
 *
 * There are two ways how to access models.
 *
 * **1. A list of all models**
 *
 * Call `app.models()` to get a list of all models.
 *
 * ```js
 * var models = app.models();
 *
 * models.forEach(function (Model) {
 *  console.log(Model.modelName); // color
 * });
 * ```
 *
 * **2. By model name**
 *
 * `app.model` has properties for all defined models.
 *
 * In the following example the `Product` and `CustomerReceipt` models are
 * accessed using the `models` object.
 *
 * ```js
 * var loopback = require('loopback');
 *  var app = loopback();
 *  app.boot({
 *   dataSources: {
 *     db: {connector: 'memory'}
 *   }
 * });
 *
 * app.model('product', {dataSource: 'db'});
 * app.model('customer-receipt', {dataSource: 'db'});
 *
 * // available based on the given name
 * var Product = app.models.Product;
 *
 * // also available as camelCase
 * var product = app.models.product;
 *
 * // multi-word models are avaiable as pascal cased
 * var CustomerReceipt = app.models.CustomerReceipt;
 *
 * // also available as camelCase
 * var customerReceipt = app.models.customerReceipt;
 * ```
 *
 * @returns {Array} a list of model classes
 */

app.models = function () {
  return this._models || (this._models = []);
}

/**
 * Define a DataSource.
 *
 * @param {String} name The data source name
 * @param {Object} config The data source config 
 */

app.dataSource = function (name, config) {
  this.dataSources[name] =
  this.dataSources[classify(name)] =
  this.dataSources[camelize(name)] = dataSourcesFromConfig(config);
}

/**
 * Get all remote objects.
 */

app.remoteObjects = function () {
  var result = {};
  var models = this.models();
  
  // add in models
  models.forEach(function (ModelCtor) {
    // only add shared models
    if(ModelCtor.shared && typeof ModelCtor.sharedCtor === 'function') {
      result[compat.getClassNameForRemoting(ModelCtor)] = ModelCtor;
    }
  });
    
  return result;
}

/**
 * Get the apps set of remote objects.
 */
 
app.remotes = function () {
  return this._remotes || (this._remotes = RemoteObjects.create());
}

/**
 * Enable swagger REST API documentation.
 *
 * > Note: This method is deprecated, use the extension
 * [loopback-explorer](http://npmjs.org/package/loopback-explorer) instead.
 *
 * **Options**
 *
 * - `basePath` The basepath for your API - eg. 'http://localhost:3000'.
 *
 * **Example**
 *
 * ```js
 * // enable docs
 * app.docs({basePath: 'http://localhost:3000'});
 * ```
 *
 * Run your app then navigate to
 * [the API explorer](http://petstore.swagger.wordnik.com/).
 * Enter your API basepath to view your generated docs.
 *
 * @deprecated
 */
 
app.docs = function (options) {
  var remotes = this.remotes();
  swagger(remotes, options);
}

/*!
 * Get a handler of the specified type from the handler cache.
 */
 
app.handler = function (type) {
  var handlers = this._handlers || (this._handlers = {});
  if(handlers[type]) {
    return handlers[type];
  }
  
  var remotes = this.remotes();
  var handler = this._handlers[type] = remotes.handler(type);
  return handler;
}

/**
 * An object to store dataSource instances.
 */

app.dataSources = app.datasources = {};

/**
 * Enable app wide authentication.
 */

app.enableAuth = function() {
  var remotes = this.remotes();

  remotes.before('**', function(ctx, next, method) {
    var req = ctx.req;
    var Model = method.ctor;
    var modelInstance = ctx.instance;
    var modelId = modelInstance && modelInstance.id || req.param('id');

    if(Model.checkAccess) {
      Model.checkAccess(
        req.accessToken,
        modelId,
        method.name,
        function(err, allowed) {
          if(err) {
            console.log(err);
            next(err);
          } else if(allowed) {
            next();
          } else {
            var e = new Error('Access Denied');
            e.statusCode = 401;
            next(e);
          }
        }
      );
    } else {
      next();
    }
  });
}

/**
 * Initialize an application from an options object or a set of JSON and JavaScript files.
 * 
 * **What happens during an app _boot_?**
 * 
 * 1. **DataSources** are created from an `options.dataSources` object or `datasources.json` in the current directory
 * 2. **Models** are created from an `options.models` object or `models.json` in the current directory
 * 3. Any JavaScript files in the `./models` directory are loaded with `require()`.
 * 4. Any JavaScript files in the `./boot` directory are loaded with `require()`.
 * 
 * **Options**
 * 
 *  - `cwd` - _optional_ - the directory to use when loading JSON and JavaScript files
 *  - `models` - _optional_ - an object containing `Model` definitions
 *  - `dataSources` - _optional_ - an object containing `DataSource` definitions
 *
 *  > **NOTE:** mixing `app.boot()` and `app.model(name, config)` in multiple
 *  > files may result
 *  > in models being **undefined** due to race conditions. To avoid this when
 *  > using `app.boot()`
 *  > make sure all models are passed as part of the `models` definition.
 *
 * <a name="model-definition"></a>
 * **Model Definitions**
 * 
 * The following is an example of an object containing two `Model` definitions: "location" and "inventory".
 * 
 * ```js
 * {
 *   "dealership": {
 *     // a reference, by name, to a dataSource definition
 *     "dataSource": "my-db",
 *     // the options passed to Model.extend(name, properties, options)
 *     "options": {
 *       "relations": {
 *         "cars": {
 *           "type": "hasMany",
 *           "model": "Car",
 *           "foreignKey": "dealerId"  
 *         }
 *       }
 *     },
 *     // the properties passed to Model.extend(name, properties, options)
 *     "properties": {
 *       "id": {"id": true},
 *       "name": "String",
 *       "zip": "Number",
 *       "address": "String"
 *     }
 *   },
 *   "car": {
 *     "dataSource": "my-db"
 *     "properties": {
 *       "id": {
 *         "type": "String",
 *         "required": true,
 *         "id": true
 *       },
 *       "make": {
 *         "type": "String",
 *         "required": true
 *       },
 *       "model": {
 *         "type": "String",
 *         "required": true
 *       }
 *     }
 *   }
 * }
 * ```
 * 
 * **Model definition properties**
 * 
 *  - `dataSource` - **required** - a string containing the name of the data source definition to attach the `Model` to
 *  - `options` - _optional_ - an object containing `Model` options
 *  - `properties` _optional_ - an object defining the `Model` properties in [LoopBack Definition Language](http://docs.strongloop.com/loopback-datasource-juggler/#loopback-definition-language)
 * 
 * **DataSource definition properties**
 *  
 *  - `connector` - **required** - the name of the [connector](#working-with-data-sources-and-connectors)
 * 
 * @header app.boot([options])
 * @throws {Error} If config is not valid
 * @throws {Error} If boot fails
 */

app.boot = function(options) {
  options = options || {};

  if(typeof options === 'string') {
    options = {appRootDir: options};
  }
  var app = this;
  var appRootDir = options.appRootDir = options.appRootDir || process.cwd();
  var ctx = {};
  var appConfig = options.app;
  var modelConfig = options.models;
  var dataSourceConfig = options.dataSources;

  if(!appConfig) {
    appConfig = tryReadConfig(appRootDir, 'app') || {};
  }
  if(!modelConfig) {
    modelConfig = tryReadConfig(appRootDir, 'models') || {};
  }
  if(!dataSourceConfig) {
    dataSourceConfig = tryReadConfig(appRootDir, 'datasources') || {};
  }

  assertIsValidConfig('app', appConfig);
  assertIsValidConfig('model', modelConfig);
  assertIsValidConfig('data source', dataSourceConfig);

  appConfig.host = 
    process.env.npm_config_host ||
    process.env.OPENSHIFT_SLS_IP ||
    process.env.OPENSHIFT_NODEJS_IP ||
    process.env.HOST ||
    appConfig.host ||
    process.env.npm_package_config_host ||
    app.get('host');

  appConfig.port = 
    process.env.npm_config_port ||
    process.env.OPENSHIFT_SLS_PORT ||
    process.env.OPENSHIFT_NODEJS_PORT ||
    process.env.PORT ||
    appConfig.port ||
    process.env.npm_package_config_port ||
    app.get('port') || 
    3000;

  appConfig.restApiRoot =
    appConfig.restApiRoot ||
    app.get('restApiRoot') ||
    '/api';

  if(appConfig.host !== undefined) {
    assert(typeof appConfig.host === 'string', 'app.host must be a string');
    app.set('host', appConfig.host);
  }

  if(appConfig.port !== undefined) {
    var portType = typeof appConfig.port;
    assert(portType === 'string' || portType === 'number', 'app.port must be a string or number');
    app.set('port', appConfig.port);
  }

  assert(appConfig.restApiRoot !== undefined, 'app.restBasePath is required');
  assert(typeof appConfig.restApiRoot === 'string', 'app.restBasePath must be a string');
  assert(/^\//.test(appConfig.restApiRoot), 'app.restBasePath must start with "/"');
  app.set('restApiRoot', appConfig.restBasePath);

  for(var configKey in appConfig) {
    var cur = app.get(configKey);
    if(cur === undefined || cur === null) {
      app.set(configKey, appConfig[configKey]);
    }
  }

  // instantiate data sources
  forEachKeyedObject(dataSourceConfig, function(key, obj) {
    app.dataSource(key, obj);
  });

  // instantiate models
  forEachKeyedObject(modelConfig, function(key, obj) {
    app.model(key, obj);
  });

  // try to attach models to dataSources by type
  try {
    require('./loopback').autoAttach();
  } catch(e) {
    if(e.name === 'AssertionError') {
      console.warn(e);
    } else {
      throw e;
    }
  }

  // disable token requirement for swagger, if available
  var swagger = app.remotes().exports.swagger;
  var requireTokenForSwagger = appConfig.swagger
                            && appConfig.swagger.requireToken;
  if(swagger) {
    swagger.requireToken = requireTokenForSwagger || false;
  }

  // require directories
  var requiredModels = requireDir(path.join(appRootDir, 'models'));
  var requiredBootScripts = requireDir(path.join(appRootDir, 'boot'));
}

function assertIsValidConfig(name, config) {
  if(config) {
    assert(typeof config === 'object', name + ' config must be a valid JSON object');  
  }
}

function forEachKeyedObject(obj, fn) {
  if(typeof obj !== 'object') return;

  Object.keys(obj).forEach(function(key) {
    fn(key, obj[key]);
  });
}

function classify(str) {
  return stringUtils.classify(str);
}

function camelize(str) {
  return stringUtils.camelize(str);
}

function dataSourcesFromConfig(config) {
  var connectorPath;

  assert(typeof config === 'object', 
    'cannont create data source without config object');

  if(typeof config.connector === 'string') {
    connectorPath = path.join(__dirname, 'connectors', config.connector+'.js');

    if(fs.existsSync(connectorPath)) {
      config.connector = require(connectorPath);
    }
  }

  return require('./loopback').createDataSource(config);
}

function modelFromConfig(name, config, app) {
  var ModelCtor = require('./loopback').createModel(name, config.properties, config.options);
  var dataSource = app.dataSources[config.dataSource];

  assert(isDataSource(dataSource), name + ' is referencing a dataSource that does not exist: "'+ config.dataSource +'"');

  ModelCtor.attachTo(dataSource);
  return ModelCtor;
}

function requireDir(dir, basenames) {
  assert(dir, 'cannot require directory contents without directory name');

  var requires = {};

  if (arguments.length === 2) {
    // if basenames argument is passed, explicitly include those files
    basenames.forEach(function (basename) {
      var filepath = Path.resolve(Path.join(dir, basename));
      requires[basename] = tryRequire(filepath);
    });
  } else if (arguments.length === 1) {
    // if basenames arguments isn't passed, require all javascript
    // files (except for those prefixed with _) and all directories

    var files = tryReadDir(dir);

    // sort files in lowercase alpha for linux
    files.sort(function (a,b) {
      a = a.toLowerCase();
      b = b.toLowerCase();

      if (a < b) {
        return -1;
      } else if (b < a) {
        return 1;
      } else {
        return 0;
      }
    });

    files.forEach(function (filename) {
      // ignore index.js and files prefixed with underscore
      if ((filename === 'index.js') || (filename[0] === '_')) { return; }

      var filepath = path.resolve(path.join(dir, filename));
      var ext = path.extname(filename);
      var stats = fs.statSync(filepath);

      // only require files supported by require.extensions (.txt .md etc.)
      if (stats.isFile() && !(ext in require.extensions)) { return; }

      var basename = path.basename(filename, ext);

      requires[basename] = tryRequire(filepath);
    });

  }

  return requires;
};

function tryRequire(modulePath) {
  try {
    return require.apply(this, arguments);
  } catch(e) {
    console.error('failed to require "%s"', modulePath);
    throw e;
  }
}

function tryReadDir() {
  try {
    return fs.readdirSync.apply(fs, arguments);
  } catch(e) {
    return [];
  }
}

function isModelCtor(obj) {
  return typeof obj === 'function' && obj.modelName && obj.name === 'ModelCtor';
}

function isDataSource(obj) {
  return obj instanceof DataSource;
}

function tryReadConfig(cwd, fileName) {
  try {
    return require(path.join(cwd, fileName + '.json'));
  } catch(e) {
    if(e.code !== "MODULE_NOT_FOUND") {
      throw e;
    }
  }
}

/**
 * Install all express middleware required by LoopBack.
 *
 * It is possible to inject your own middleware by listening on one of the
 * following events:
 *
 *  - `middleware:preprocessors` is emitted after all other
 *    request-preprocessing middleware was installed, but before any
 *    request-handling middleware is configured.
 *
 *    Usage:
 *    ```js
 *    app.once('middleware:preprocessors', function() {
 *      app.use(loopback.limit('5.5mb'))
 *    });
 *    ```
 *  - `middleware:handlers` is emitted when it's time to add your custom
 *    request-handling middleware. Note that you should not install any
 *    express routes at this point (express routes are discussed later).
 *
 *    Usage:
 *    ```js
 *    app.once('middleware:handlers', function() {
 *      app.use('/admin', adminExpressApp);
 *      app.use('/custom', function(req, res, next) {
 *        res.send(200, { url: req.url });
 *      });
 *    });
 *    ```
 *  - `middleware:error-loggers` is emitted at the end, before the loopback
 *    error handling middleware is installed. This is the point where you
 *    can install your own middleware to log errors.
 *
 *    Notes:
 *     - The middleware function must take four parameters, otherwise it won't
 *       be called by express.
 *
 *     - It should also call `next(err)` to let the loopback error handler convert
 *       the error to an HTTP error response.
 *
 *    Usage:
 *    ```js
 *    var bunyan = require('bunyan');
 *    var log = bunyan.createLogger({name: "myapp"});
 *    app.once('middleware:error-loggers', function() {
 *      app.use(function(err, req, res, next) {
 *        log.error(err);
 *        next(err);
 *      });
 *    });
 *    ```
 *
 * Express routes should be added after `installMiddleware` was called.
 * This way the express router middleware is injected at the right place in the
 * middleware chain. If you add an express route before calling this function,
 * bad things will happen: Express will automatically add the router
 * middleware and since we haven't added request-preprocessing middleware like
 * cookie & body parser yet, your route handlers will receive raw unprocessed
 * requests.
 *
 * This is the correct order in which to call `app` methods:
 * ```js
 * app.boot(__dirname); // optional
 *
 * app.installMiddleware();
 *
 * // [register your express routes here]
 *
 * app.listen();
 * ```
 */
app.installMiddleware = function() {
  var loopback = require('../');

  /*
   * Request pre-processing
   */
  this.use(loopback.favicon());
  // TODO(bajtos) refactor to app.get('loggerFormat')
  var loggerFormat = this.get('env') === 'development' ? 'dev' : 'default';
  this.use(loopback.logger(loggerFormat));
  this.use(loopback.cookieParser(this.get('cookieSecret')));
  this.use(loopback.token({ model: this.models.accessToken }));
  this.use(loopback.bodyParser());
  this.use(loopback.methodOverride());

  // Allow the app to install custom preprocessing middleware
  this.emit('middleware:preprocessors');

  /*
   * Request handling
   */

  // LoopBack REST transport
  this.use(this.get('restApiRoot') || '/api', loopback.rest());

  // Allow the app to install custom request handling middleware
  this.emit('middleware:handlers');

  // Let express routes handle requests that were not handled
  // by any of the middleware registered above.
  // This way LoopBack REST and API Explorer take precedence over
  // express routes.
  this.use(this.router);

  // The static file server should come after all other routes
  // Every request that goes through the static middleware hits
  // the file system to check if a file exists.
  this.use(loopback.static(path.join(__dirname, 'public')));

  // Requests that get this far won't be handled
  // by any middleware. Convert them into a 404 error
  // that will be handled later down the chain.
  this.use(loopback.urlNotFound());

  /*
   * Error handling
   */

  // Allow the app to install custom error logging middleware
  this.emit('middleware:error-handlers');

  // The ultimate error handler.
  this.use(loopback.errorHandler());
};

/**
 * Listen for connections and update the configured port.
 *
 * When there are no parameters or there is only one callback parameter,
 * the server will listen on `app.get('host')` and `app.get('port')`.
 *
 * ```js
 * // listen on host/port configured in app config
 * app.listen();
 * ```
 *
 * Otherwise all arguments are forwarded to `http.Server.listen`.
 *
 * ```js
 * // listen on the specified port and all hosts, ignore app config
 * app.listen(80);
 * ```
 *
 * The function also installs a `listening` callback that calls
 * `app.set('port')` with the value returned by `server.address().port`.
 * This way the port param contains always the real port number, even when
 * listen was called with port number 0.
 *
 * @param {Function=} cb If specified, the callback will be added as a listener
 *   for the server's "listening" event.
 * @returns {http.Server} A node `http.Server` with this application configured
 *   as the request handler.
 */
app.listen = function(cb) {
  var self = this;

  var server = require('http').createServer(this);

  server.on('listening', function() {
    self.set('port', this.address().port);
  });

  var useAppConfig =
    arguments.length == 0 ||
      (arguments.length == 1 && typeof arguments[0] == 'function');

  if (useAppConfig) {
    server.listen(this.get('port'), this.get('host'), cb);
  } else {
    server.listen.apply(server, arguments);
  }

  return server;
}