// Copyright IBM Corp. 2014,2019. All Rights Reserved.
// Node module: loopback-connector
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

'use strict';
const SG = require('strong-globalize');
const g = SG();
const debug = require('debug')('loopback:connector');

module.exports = Connector;

/**
 * Base class for LoopBack connector. This is more a collection of useful
 * methods for connectors than a super class
 * @class
 */
function Connector(name, settings) {
  this._models = {};
  this.name = name;
  this.settings = settings || {};
}

/**
 * @private
 * Set the relational property to indicate the backend is a relational DB
 * @type {boolean}
 */
Connector.prototype.relational = false;

/**
 * Check if the connector is for a relational DB
 * @returns {Boolean} true for relational DB
 */
Connector.prototype.isRelational = function() {
  return this.relational ||
    (this.getTypes().indexOf('rdbms') !== -1);
};

/**
 * Get types associated with the connector
 * @returns {String[]} The types for the connector
 */
Connector.prototype.getTypes = function() {
  return ['db', 'nosql'];
};

/**
 * Get the default data type for ID
 * @param {Object} prop Property definition
 * @returns {Function} The default type for ID
 */
Connector.prototype.getDefaultIdType = function(prop) {
  /* jshint unused:false */
  return String;
};

/**
 * Generate random id.  Each data source model must override this method.
 * @param {String} modelName Model name
 * @returns {*} Data type varies from model to model,
 */

Connector.prototype.generateUniqueId = function(modelName) {
  const idType = this.getDefaultIdType && this.getDefaultIdType();
  const isTypeFunction = (typeof idType === 'function');
  const id = this.generateValueByColumnType ? this.generateValueByColumnType(idType) :
    (typeof idType === 'function' ? idType() : null);
  return id;
};

/**
 * Get the metadata for the connector
 * @returns {Object} The metadata object
 * @property {String} type The type for the backend
 * @property {Function} defaultIdType The default id type
 * @property {Boolean} [isRelational] If the connector represents a relational
 * database
 * @property {Object} schemaForSettings The schema for settings object
 */
Connector.prototype.getMetadata = function() {
  if (!this._metadata) {
    this._metadata = {
      types: this.getTypes(),
      defaultIdType: this.getDefaultIdType(),
      isRelational: this.isRelational(),
      multiInsertSupported: !!this.multiInsertSupported,
      schemaForSettings: {},
    };
  }
  return this._metadata;
};

/**
 * Execute a command with given parameters
 * @param {String|Object} command The command such as SQL
 * @param {Array} [params] An array of parameter values
 * @param {Object} [options] Options object
 * @param {Function} [callback] The callback function
 */
Connector.prototype.execute = function(command, params, options, callback) {
  throw new Error(g.f('execute() must be implemented by the connector'));
};

/**
 * Get the model definition by name
 * @param {String} modelName The model name
 * @returns {ModelDefinition} The model definition
 */
Connector.prototype.getModelDefinition = function(modelName) {
  return this._models[modelName];
};

/**
 * Get connector specific settings for a given model, for example,
 * ```
 * {
 *   "postgresql": {
 *     "schema": "xyz"
 *   }
 * }
 * ```
 *
 * @param {String} modelName Model name
 * @returns {Object} The connector specific settings
 */
Connector.prototype.getConnectorSpecificSettings = function(modelName) {
  const settings = this.getModelDefinition(modelName).settings || {};
  return settings[this.name];
};

/**
 * Get model property definition
 * @param {String} modelName Model name
 * @param {String} propName Property name
 * @returns {Object} Property definition
 */
Connector.prototype.getPropertyDefinition = function(modelName, propName) {
  const model = this.getModelDefinition(modelName);
  return Connector.getNestedPropertyDefinition(
    model.model.definition,
    propName.split('.'),
  );
};

/**
 * Helper function to get nested property definition
 * @param {Object} definition Model name
 * @param {Array} propPath
 * @returns {Object} Property definition
 */
Connector.getNestedPropertyDefinition = function(definition, propPath) {
  const properties = definition.properties || {};
  const prop = properties[propPath[0]];
  const isPropUndefined = typeof prop === 'undefined';
  const isArray = !isPropUndefined && Array.isArray(prop.type);
  const isFunction = !isPropUndefined && !isArray && typeof prop.type === 'function';

  if (propPath.length === 1) return prop;

  if (isPropUndefined || (propPath.length > 1 && (isArray && prop.type.length === 0))) {
    return undefined;
  }

  const nextDefinition =
    (isArray && prop.type[0].definition) ||
    (isFunction && prop.type.definition);

  if (nextDefinition === undefined) {
    return undefined;
  } else {
    return Connector.getNestedPropertyDefinition(
      nextDefinition,
      propPath.slice(1),
    );
  }
};

/**
 * Look up the data source by model name
 * @param {String} model The model name
 * @returns {DataSource} The data source
 */
Connector.prototype.getDataSource = function(model) {
  const m = this.getModelDefinition(model);
  if (!m) {
    debug('Model not found: ' + model);
  }
  return m && m.model.dataSource;
};

/**
 * Get the id property name
 * @param {String} model The model name
 * @returns {String} The id property name
 */
Connector.prototype.idName = function(model) {
  return this.getDataSource(model).idName(model);
};

/**
 * Get the id property names
 * @param {String} model The model name
 * @returns {String[]} The id property names
 */
Connector.prototype.idNames = function(model) {
  return this.getDataSource(model).idNames(model);
};

/**
 * Get the id index (sequence number, starting from 1)
 * @param {String} model The model name
 * @param {String} prop The property name
 * @returns {Number} The id index, undefined if the property is not part
 *   of the primary key
 */
Connector.prototype.id = function(model, prop) {
  const p = this.getModelDefinition(model).properties[prop];
  return p && p.id;
};

/**
 * Return the database name of the property of the model if it exists.
 * Otherwise return the property name.
 * Some connectors allow the column/field name to be customized
 * at the model property definition level as `column`,
 * `columnName`, or `field`. For example,
 *
 * ```json
 * "name": {
 *   "type": "string",
 *   "mysql": {
 *     "column": "NAME"
 *   }
 * }
 * ```
 * @param {String} model The target model name
 * @param {String} prop The property name
 *
 * @returns {String} The database mapping name of the property of the model if it exists
 */
Connector.prototype.getPropertyDbName = Connector.prototype.column =
function(model, property) {
  const prop = this.getPropertyDefinition(model, property);
  let mappingName;
  if (prop && prop[this.name]) {
    mappingName = prop[this.name].column || prop[this.name].columnName ||
    prop[this.name].field || prop[this.name].fieldName;
    if (mappingName) {
      // Explicit column name, return as-is
      return mappingName;
    }
  }

  // Check if name attribute provided for column name
  if (prop && prop.name) {
    return prop.name;
  }
  mappingName = property;
  if (typeof this.dbName === 'function') {
    mappingName = this.dbName(mappingName);
  }
  return mappingName;
};

/**
 * Return the database name of the id property of the model if it exists.
 * Otherwise return the name of the id property.
 * @param {String} model The target model name
 * @param {String} prop The property name
 * @returns {String} the database mapping name of the id property of the model if it exists.
 */
Connector.prototype.getIdDbName = Connector.prototype.idColumn = function(model) {
  const idName = this.getDataSource(model).getModelDefinition(model).idName();
  return this.getPropertyDbName(model, idName);
};

/**
 * Hook to be called by DataSource for defining a model
 * @param {Object} modelDefinition The model definition
 */
Connector.prototype.define = function(modelDefinition) {
  modelDefinition.settings = modelDefinition.settings || {};
  this._models[modelDefinition.model.modelName] = modelDefinition;
};

/**
 * Hook to be called by DataSource for defining a model property
 * @param {String} model The model name
 * @param {String} propertyName The property name
 * @param {Object} propertyDefinition The object for property definition
 */
Connector.prototype.defineProperty = function(model, propertyName, propertyDefinition) {
  const modelDef = this.getModelDefinition(model);
  modelDef.properties[propertyName] = propertyDefinition;
};

/**
 * Disconnect from the connector
 * @param {Function} [cb] Callback function
 */
Connector.prototype.disconnect = function disconnect(cb) {
  // NO-OP
  if (cb) {
    process.nextTick(cb);
  }
};

/**
 * Get the id value for the given model
 * @param {String} model The model name
 * @param {Object} data The model instance data
 * @returns {*} The id value
 *
 */
Connector.prototype.getIdValue = function(model, data) {
  return data && data[this.idName(model)];
};

/**
 * Set the id value for the given model
 * @param {String} model The model name
 * @param {Object} data The model instance data
 * @param {*} value The id value
 *
 */
Connector.prototype.setIdValue = function(model, data, value) {
  if (data) {
    data[this.idName(model)] = value;
  }
};

/**
 * Test if a property is nullable
 * @param {Object} prop The property definition
 * @returns {boolean} true if nullable
 */
Connector.prototype.isNullable = function(prop) {
  if (prop.required || prop.id)
    return false;

  const nullableFlagsValue = [prop.nullable, prop['null'], prop.allowNull];
  const notNullableValues = [0, 'N', 'NO', false];

  for (const flagValue of nullableFlagsValue) {
    if (notNullableValues.includes(flagValue))
      return false;
  }

  return true;
};

/**
 * Return the DataAccessObject interface implemented by the connector
 * @returns {Object} An object containing all methods implemented by the
 * connector that can be mixed into the model class. It should be considered as
 * the interface.
 */
Connector.prototype.getDataAccessObject = function() {
  return this.DataAccessObject;
};

/*!
 * Define aliases to a prototype method/property
 * @param {Function} cls The class that owns the method/property
 * @param {String} methodOrPropertyName The official property method/property name
 * @param {String|String[]} aliases Aliases to the official property/method
 */
Connector.defineAliases = function(cls, methodOrPropertyName, aliases) {
  if (typeof aliases === 'string') {
    aliases = [aliases];
  }
  if (Array.isArray(aliases)) {
    aliases.forEach(function(alias) {
      if (typeof alias === 'string') {
        Object.defineProperty(cls, alias, {
          get: function() {
            return this[methodOrPropertyName];
          },
        });
      }
    });
  }
};

/**
 * `command()` and `query()` are aliases to `execute()`
 */
Connector.defineAliases(Connector.prototype, 'execute', ['command', 'query']);