'use strict';

/**
 * Module dependencies.
 */

var debug = require('debug')('loopback:explorer:routeHelpers');
var _assign = require('lodash').assign;
var typeConverter = require('./type-converter');
var schemaBuilder = require('./schema-builder');

/**
 * Export the routeHelper singleton.
 */
var routeHelper = module.exports = {
  /**
   * Given a route, generate an API description and add it to the doc.
   * If a route shares a path with another route (same path, different verb),
   * add it as a new operation under that path entry.
   *
   * Routes can be translated to API declaration 'operations',
   * but they need a little massaging first. The `accepts` and
   * `returns` declarations need some basic conversions to be compatible.
   *
   * This method will convert the route and add it to the doc.
   * @param  {Route} route    Strong Remoting Route object.
   * @param  {Class} classDef Strong Remoting class.
   * @param  {TypeRegistry} typeRegistry Registry of types and models.
   * @param  {Object} paths   Swagger Path Object,
   *   see https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#pathsObject
   */
  addRouteToSwaggerPaths: function(route, classDef, typeRegistry, paths) {
    var entryToAdd = routeHelper.routeToPathEntry(route, classDef,
                                                  typeRegistry);
    if (!(entryToAdd.path in paths)) {
      paths[entryToAdd.path] = {};
    }
    paths[entryToAdd.path][entryToAdd.method] = entryToAdd.operation;
  },

  /**
   * Massage route.accepts.
   * @param  {Object} route    Strong Remoting Route object.
   * @param  {Class}  classDef Strong Remoting class.
   * @param  {TypeRegistry} typeRegistry Registry of types and models.
   * @return {Array}           Array of param docs.
   */
  convertAcceptsToSwagger: function(route, classDef, typeRegistry) {
    var accepts = route.accepts || [];
    var split = route.method.split('.');
    if (classDef && classDef.sharedCtor &&
        classDef.sharedCtor.accepts && split.length > 2 /* HACK */) {
      accepts = accepts.concat(classDef.sharedCtor.accepts);
    }

    // Filter out parameters that are generated from the incoming request,
    // or generated by functions that use those resources.
    accepts = accepts.filter(function(arg) {
      if (!arg.http) return true;
      // Don't show derived arguments.
      if (typeof arg.http === 'function') return false;
      // Don't show arguments set to the incoming http request.
      // Please note that body needs to be shown, such as User.create().
      if (arg.http.source === 'req' ||
        arg.http.source === 'res' ||
        arg.http.source === 'context') {

        return false;
      }
      return true;
    });

    // Turn accept definitions in to parameter docs.
    accepts = accepts.map(
      routeHelper.acceptToParameter(route, classDef, typeRegistry));

    return accepts;
  },

  /**
   * Massage route.returns.
   * @param  {Object} route    Strong Remoting Route object.
   * @return {Object}          A single returns param doc.
   */
  convertReturnsToSwagger: function(route, typeRegistry) {
    var routeReturns = route.returns;
    if (!routeReturns || !routeReturns.length) {
      // An operation that returns nothing will have
      // no schema declaration for its response.
      return undefined;
    }

    if (routeReturns.length === 1 && routeReturns[0].root) {
      if (routeReturns[0].model)
        return { $ref: typeRegistry.reference(routeReturns[0].model) };
      return schemaBuilder.buildFromLoopBackType(routeReturns[0], typeRegistry);
    }

    // Convert `returns` into a single object for later conversion into an
    // operation object.
    // TODO ad-hoc model definition in the case of multiple return values.
    // It is enough to replace 'object' with an anonymous type definition
    // based on all routeReturn items. The schema converter should take
    // care of the remaning conversions.
    var def = { type: 'object' };
    return schemaBuilder.buildFromLoopBackType(def, typeRegistry);
  },

  /**
   * Converts from an sl-remoting-formatted "Route" description to a
   * Swagger-formatted "Path Item Object"
   * See swagger-spec/2.0.md#pathItemObject
   */
  routeToPathEntry: function(route, classDef, typeRegistry) {
    // Some parameters need to be altered; eventually most of this should
    // be removed.
    var accepts = routeHelper.convertAcceptsToSwagger(route, classDef,
                                                      typeRegistry);
    var returns = routeHelper.convertReturnsToSwagger(route, typeRegistry);
    var defaultCode = route.returns && route.returns.length ? 200 : 204;
    // TODO - support strong-remoting's option for a custom response code

    var responseMessages = {};
    responseMessages[defaultCode] = {
      description: 'Request was successful',
      schema: returns,
      // TODO - headers, examples
    };

    if (route.errors) {
      // TODO define new LDL syntax that is status-code-indexed
      // and which allow users to specify headers & examples
      route.errors.forEach(function(msg) {
        responseMessages[msg.code] = {
          description: msg.message,
          schema: schemaBuilder.buildFromLoopBackType(msg.responseModel,
                                                      typeRegistry),
          // TODO - headers, examples
        };
      });
    }

    debug('route %j', route);

    var tags = [];
    if (classDef && classDef.name) {
      tags.push(classDef.name);
    }

    var entry = {
      path: routeHelper.convertPathFragments(route.path),
      method: routeHelper.convertVerb(route.verb),
      operation: {
        tags: tags,
        summary: typeConverter.convertText(route.description),
        description: typeConverter.convertText(route.notes),
        // [bajtos] We used to remove leading model name from the operation
        // name for Swagger Spec 1.2. Swagger Spec 2.0 requires
        // operation ids to be unique, thus we have to include the model name.
        operationId: route.method,
        // [bajtos] we are omitting consumes and produces, as they are same
        // for all methods and they are already specified in top-level fields
        parameters: accepts,
        responses: responseMessages,
        deprecated: !!route.deprecated,
        // TODO: security
      }
    };

    return entry;
  },

  convertPathFragments: function convertPathFragments(path) {
    return path.split('/').map(function (fragment) {
      if (fragment.charAt(0) === ':') {
        return '{' + fragment.slice(1) + '}';
      }
      return fragment;
    }).join('/');
  },

  convertVerb: function convertVerb(verb) {
    if (verb.toLowerCase() === 'all') {
      return 'post';
    }

    if (verb.toLowerCase() === 'del') {
      return 'delete';
    }

    return verb.toLowerCase();
  },

  /**
   * A generator to convert from an sl-remoting-formatted "Accepts" description
   * to a Swagger-formatted "Parameter" description.
   */
  acceptToParameter: function acceptToParameter(route, classDef, typeRegistry) {
    var DEFAULT_TYPE =
      route.verb.toLowerCase() === 'get' ?  'query' : 'formData';

    return function (accepts) {
      var name = accepts.name || accepts.arg;
      var paramType = DEFAULT_TYPE;

      // TODO: Regex. This is leaky.
      if (route.path.indexOf(':' + name) !== -1) {
        paramType = 'path';
      }

      // Check the http settings for the argument
      if (accepts.http && accepts.http.source) {
        paramType = accepts.http.source;
      }

      // TODO: ensure that paramType has a valid value
      //  path, query, header, body, formData
      // See swagger-spec/2.0.md#parameterObject

      var paramObject = {
        name: name,
        in: paramType,
        description: typeConverter.convertText(accepts.description),
        required: !!accepts.required
      };

      var schema = schemaBuilder.buildFromLoopBackType(accepts, typeRegistry);
      if (paramType === 'body') {
        // HACK: Derive the type from model
        if (paramObject.name === 'data' && schema.type === 'object') {
          paramObject.schema = { $ref: typeRegistry.reference(classDef.name) };
        } else {
          paramObject.schema = schema;
        }
      } else {
        var isComplexType = schema.type === 'object' ||
                            schema.type === 'array' ||
                            schema.$ref;
        if (isComplexType) {
          paramObject.type = 'string';
          paramObject.format = 'JSON';
          // TODO support array of primitive types
          // and map them to Swagger array of primitive types
        } else {
          _assign(paramObject, schema);
        }
      }

      return paramObject;
    };
  },
};