/*jshint strict: false, unused: false, maxlen: 200 */

// //////////////////////////////////////////////////////////////////////////////
// / DISCLAIMER
// /
// / Copyright 2014-2024 ArangoDB GmbH, Cologne, Germany
// / Copyright 2004-2014 triAGENS GmbH, Cologne, Germany
// /
// / Licensed under the Business Source License 1.1 (the "License");
// / you may not use this file except in compliance with the License.
// / You may obtain a copy of the License at
// /
// /     https://github.com/arangodb/arangodb/blob/devel/LICENSE
// /
// / Unless required by applicable law or agreed to in writing, software
// / distributed under the License is distributed on an "AS IS" BASIS,
// / WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// / See the License for the specific language governing permissions and
// / limitations under the License.
// /
// / Copyright holder is ArangoDB GmbH, Cologne, Germany
// /
// //////////////////////////////////////////////////////////////////////////////

var ArangoCollection = require('@arangodb/arango-collection').ArangoCollection;

var arangodb = require('@arangodb');

var ArangoError = arangodb.ArangoError;
var sprintf = arangodb.sprintf;

var simple = require('@arangodb/simple-query');

var SimpleQueryAll = simple.SimpleQueryAll;
var SimpleQueryByExample = simple.SimpleQueryByExample;
var SimpleQueryByCondition = simple.SimpleQueryByCondition;
var SimpleQueryRange = simple.SimpleQueryRange;
var SimpleQueryGeo = simple.SimpleQueryGeo;
var SimpleQueryNear = simple.SimpleQueryNear;
var SimpleQueryWithin = simple.SimpleQueryWithin;
var SimpleQueryWithinRectangle = simple.SimpleQueryWithinRectangle;
var SimpleQueryFulltext = simple.SimpleQueryFulltext;

// //////////////////////////////////////////////////////////////////////////////
// / @brief collection is corrupted
// //////////////////////////////////////////////////////////////////////////////

ArangoCollection.STATUS_CORRUPTED = 0;

// //////////////////////////////////////////////////////////////////////////////
// / @brief collection is loaded
// //////////////////////////////////////////////////////////////////////////////

ArangoCollection.STATUS_LOADED = 3;

// //////////////////////////////////////////////////////////////////////////////
// / @brief collection is deleted
// //////////////////////////////////////////////////////////////////////////////

ArangoCollection.STATUS_DELETED = 5;

/// note: the following collection statuses are not used by arangod anymore.
/// they are only here for backwards-compatibility, but they will be rmeoved
/// in a future version
ArangoCollection.STATUS_NEW_BORN = 1;
ArangoCollection.STATUS_UNLOADED = 2;
ArangoCollection.STATUS_UNLOADING = 4;
ArangoCollection.STATUS_LOADING = 6;

// //////////////////////////////////////////////////////////////////////////////
// / @brief document collection
// //////////////////////////////////////////////////////////////////////////////

ArangoCollection.TYPE_DOCUMENT = 2;

// //////////////////////////////////////////////////////////////////////////////
// / @brief edge collection
// //////////////////////////////////////////////////////////////////////////////

ArangoCollection.TYPE_EDGE = 3;

ArangoCollection.prototype.isArangoCollection = true;

// //////////////////////////////////////////////////////////////////////////////
// / @brief prints a collection
// //////////////////////////////////////////////////////////////////////////////

ArangoCollection.prototype._PRINT = function (context) {
  var status = 'unknown';
  var type = 'unknown';
  var name = this.name();

  switch (this.status()) {
    case ArangoCollection.STATUS_LOADED:
      status = 'loaded';
      break;
    case ArangoCollection.STATUS_CORRUPTED:
      status = 'corrupted';
      break;
    case ArangoCollection.STATUS_DELETED:
      status = 'deleted';
      break;
  }

  switch (this.type()) {
    case ArangoCollection.TYPE_DOCUMENT:
      type = 'document';
      break;
    case ArangoCollection.TYPE_EDGE:
      type = 'edge';
      break;
  }

  var colors = require('internal').COLORS;
  var useColor = context.useColor;

  context.output += '[ArangoCollection ';
  if (useColor) { context.output += colors.COLOR_NUMBER; }
  context.output += this._id;
  if (useColor) { context.output += colors.COLOR_RESET; }
  context.output += ', "';
  if (useColor) { context.output += colors.COLOR_STRING; }
  context.output += name || 'unknown';
  if (useColor) { context.output += colors.COLOR_RESET; }
  context.output += '" (type ' + type + ', status ' + status + ')]';
};

// //////////////////////////////////////////////////////////////////////////////
// / @brief converts into a string
// //////////////////////////////////////////////////////////////////////////////

ArangoCollection.prototype.toString = function () {
  return '[ArangoCollection: ' + this._id + ']';
};

// //////////////////////////////////////////////////////////////////////////////
// / @brief qualifies a given document key
// //////////////////////////////////////////////////////////////////////////////

ArangoCollection.prototype.documentId = function (documentKey) {
  if (documentKey && typeof documentKey !== "string") {
    documentKey = String(documentKey);
  }
  if (!arangodb.isValidDocumentKey(documentKey)) {
    throw new ArangoError({
      errorNum: arangodb.errors.ERROR_ARANGO_DOCUMENT_KEY_BAD.code,
      errorMessage: arangodb.errors.ERROR_ARANGO_DOCUMENT_KEY_BAD.message
    });
  }
  return `${this.name()}/${documentKey}`;
};

ArangoCollection.prototype.all = function () {
  return new SimpleQueryAll(this);
};

ArangoCollection.prototype.byExample = function (example) {
  var e;
  var i;

  // example is given as only argument
  if (arguments.length === 1) {
    e = example;
  }

  // example is given as list
  else {
    e = {};

    // create a REAL array, otherwise JSON.stringify will fail
    for (i = 0;  i < arguments.length;  i += 2) {
      e[arguments[i]] = arguments[i + 1];
    }
  }

  return new SimpleQueryByExample(this, e);
};

ArangoCollection.prototype.range = function (name, left, right) {
  return new SimpleQueryRange(this, name, left, right, 0);
};

ArangoCollection.prototype.closedRange = function (name, left, right) {
  return new SimpleQueryRange(this, name, left, right, 1);
};

ArangoCollection.prototype.geo = function (loc, order) {
  var idx;

  var locateGeoIndex1 = function (collection, loc, order) {
    var inds = collection.indexes();
    var i;

    for (i = 0;  i < inds.length;  ++i) {
      var index = inds[i];

      if (index.type === 'geo1' || (index.type === 'geo' && index.fields.length === 1)) {
        if (index.fields[0] === loc && index.geoJson === order) {
          return index;
        }
      }
    }

    return null;
  };

  var locateGeoIndex2 = function (collection, lat, lon) {
    var inds = collection.indexes();
    var i;

    for (i = 0;  i < inds.length;  ++i) {
      var index = inds[i];

      if (index.type === 'geo2' || (index.type === 'geo' && index.fields.length === 2)) {
        if (index.fields[0] === lat && index.fields[1] === lon) {
          return index;
        }
      }
    }

    return null;
  };

  if (order === undefined) {
    if (typeof loc === 'object') {
      idx = this.index(loc);
    }else {
      idx = locateGeoIndex1(this, loc, false);
    }
  }
  else if (typeof order === 'boolean') {
    idx = locateGeoIndex1(this, loc, order);
  }else {
    idx = locateGeoIndex2(this, loc, order);
  }

  if (idx === null) {
    var err = new ArangoError();
    err.errorNum = arangodb.errors.ERROR_QUERY_GEO_INDEX_MISSING.code;
    err.errorMessage = require('internal').sprintf(arangodb.errors.ERROR_QUERY_GEO_INDEX_MISSING.message, this.name());
    throw err;
  }

  return new SimpleQueryGeo(this, idx.id);
};

ArangoCollection.prototype.near = function (lat, lon) {
  return new SimpleQueryNear(this, lat, lon);
};

ArangoCollection.prototype.within = function (lat, lon, radius) {
  return new SimpleQueryWithin(this, lat, lon, radius);
};

ArangoCollection.prototype.withinRectangle = function (lat1, lon1, lat2, lon2) {
  return new SimpleQueryWithinRectangle(this, lat1, lon1, lat2, lon2);
};

ArangoCollection.prototype.fulltext = function (attribute, query, iid) {
  return new SimpleQueryFulltext(this, attribute, query, iid);
};

ArangoCollection.prototype.iterate = function (iterator, options) {
  var probability = 1.0;
  var limit = null;
  var stmt;
  var cursor;
  var pos;

  // TODO: this is not optimal for the client, there should be an HTTP call handling
  // everything on the server

  if (options !== undefined) {
    if (options.hasOwnProperty('probability')) {
      probability = options.probability;
    }

    if (options.hasOwnProperty('limit')) {
      limit = options.limit;
    }
  }

  if (limit === null) {
    if (probability >= 1.0) {
      cursor = this.all();
    }else {
      stmt = sprintf('FOR d IN %s FILTER rand() <= @prob RETURN d', this.name());
      stmt = arangodb.db._createStatement({ query: stmt });

      if (probability < 1.0) {
        stmt.bind('prob', probability);
      }

      cursor = stmt.execute();
    }
  }else {
    if (typeof limit !== 'number') {
      var error = new ArangoError();
      error.errorNum = arangodb.errors.ERROR_ILLEGAL_NUMBER.code;
      error.errorMessage = 'expecting a number, got ' + String(limit);

      throw error;
    }

    if (probability >= 1.0) {
      cursor = this.all().limit(limit);
    }else {
      stmt = sprintf('FOR d IN %s FILTER rand() <= @prob LIMIT %d RETURN d',
        this.name(), limit);
      stmt = arangodb.db._createStatement({ query: stmt });

      if (probability < 1.0) {
        stmt.bind('prob', probability);
      }

      cursor = stmt.execute();
    }
  }

  pos = 0;

  while (cursor.hasNext()) {
    var document = cursor.next();

    iterator(document, pos);

    pos++;
  }
};

ArangoCollection.prototype.removeByExample = function (example, waitForSync, limit) {
  throw 'cannot call abstract removeByExample function';
};

ArangoCollection.prototype.replaceByExample = function (example, newValue, waitForSync, limit) {
  throw 'cannot call abstract replaceByExample function';
};

ArangoCollection.prototype.updateByExample = function (example, newValue, keepNull, waitForSync, limit) {
  throw 'cannot call abstract updateExample function';
};


// //////////////////////////////////////////////////////////////////////////////
// / SECTION Indexes
// //////////////////////////////////////////////////////////////////////////////

// //////////////////////////////////////////////////////////////////////////////
// / @brief add options from arguments to index specification
// //////////////////////////////////////////////////////////////////////////////

function addIndexOptions (body, parameters) {
  body.fields = [];

  var setOption = function (k) {
    if (! body.hasOwnProperty(k)) {
      body[k] = parameters[i][k];
    }
  };

  var i;
  for (i = 0; i < parameters.length; ++i) {
    if (typeof parameters[i] === 'string') {
      // set fields
      body.fields.push(parameters[i]);
    }
    else if (typeof parameters[i] === 'object' &&
      ! Array.isArray(parameters[i]) &&
      parameters[i] !== null) {
      // set arbitrary options
      Object.keys(parameters[i]).forEach(setOption);
      break;
    }
  }

  return body;
}

// //////////////////////////////////////////////////////////////////////////////
// / @brief ensures a hash index
// //////////////////////////////////////////////////////////////////////////////

ArangoCollection.prototype.ensureHashIndex = function () {
  'use strict';

  return this.ensureIndex(addIndexOptions({
    type: 'hash',
  }, arguments));
};

// //////////////////////////////////////////////////////////////////////////////
// / @brief ensures a unique constraint
// //////////////////////////////////////////////////////////////////////////////

ArangoCollection.prototype.ensureUniqueConstraint = function () {
  'use strict';

  return this.ensureIndex(addIndexOptions({
    type: 'hash',
    unique: true
  }, arguments));
};

// //////////////////////////////////////////////////////////////////////////////
// / @brief ensures a unique skip-list index
// //////////////////////////////////////////////////////////////////////////////

ArangoCollection.prototype.ensureUniqueSkiplist = function () {
  'use strict';

  return this.ensureIndex(addIndexOptions({
    type: 'skiplist',
    unique: true
  }, arguments));
};

// //////////////////////////////////////////////////////////////////////////////
// / @brief ensures a skip-list index
// //////////////////////////////////////////////////////////////////////////////

ArangoCollection.prototype.ensureSkiplist = function () {
  'use strict';

  return this.ensureIndex(addIndexOptions({
    type: 'skiplist',
    unique: false
  }, arguments));
};

// //////////////////////////////////////////////////////////////////////////////
// / @brief ensures a fulltext index
// //////////////////////////////////////////////////////////////////////////////

ArangoCollection.prototype.ensureFulltextIndex = function (field, minLength) {
  'use strict';

  if (! Array.isArray(field)) {
    field = [ field ];
  }

  return this.ensureIndex({
    type: 'fulltext',
    minLength: minLength || undefined,
    fields: field
  });
};


// //////////////////////////////////////////////////////////////////////////////
// / @brief ensures a geo index
// //////////////////////////////////////////////////////////////////////////////

ArangoCollection.prototype.ensureGeoIndex = function (lat, lon) {
  'use strict';

  if (typeof lat !== 'string') {
    throw 'usage: ensureGeoIndex(<lat>, <lon>) or ensureGeoIndex(<loc>[, <geoJson>[, <pointsOnly>]])';
  }

  if (typeof lon === 'boolean') {
    return this.ensureIndex({
      type: 'geo1',
      fields: [ lat ],
      geoJson: lon
    });
  }

  if (lon === undefined) {
    return this.ensureIndex({
      type: 'geo1',
      fields: [ lat ],
      geoJson: false
    });
  }

  return this.ensureIndex({
    type: 'geo2',
    fields: [ lat, lon ]
  });
};

// //////////////////////////////////////////////////////////////////////////////
// / @brief ensures a geo constraint
// / since ArangoDB 2.5, this is just a redirection to ensureGeoIndex
// //////////////////////////////////////////////////////////////////////////////

ArangoCollection.prototype.ensureGeoConstraint = function (lat, lon) {
  return this.ensureGeoIndex(lat, lon);
};

// //////////////////////////////////////////////////////////////////////////////
// / @brief ensures a vertex-centric index
// //////////////////////////////////////////////////////////////////////////////

ArangoCollection.prototype.ensureVertexCentricIndex = function (...fields) {
  let options = fields.pop();
  if (!typeof options === 'object' || Array.isArray(options)) {
    let err = new ArangoError();
    err.errorNum = arangodb.errors.ERROR_BAD_PARAMETER.code;
    err.errorMessage = 'usage: ensureVertexCentricIndex(<...fields>, options), options has to be an object';
    throw err;
  }
  let dir = options.direction;
  if (typeof dir === 'string') {
    dir = dir.toLowerCase();
  }
  if (fields.length === 0 && options.hasOwnProperty("fields") && Array.isArray(options.fields)) {
    // This copies the content of the array.
    fields = options.fields.slice(0);
  }
  if (fields.length === 0) {
    let err = new ArangoError();
    err.errorNum = arangodb.errors.ERROR_BAD_PARAMETER.code;
    err.errorMessage = 'usage: ensureVertexCentricIndex(<...fields>, options), we need a non empty list of fields';
    throw err;
  }
  switch (dir) {
    case "outbound":
      fields.unshift("_from");
      break;
    case "inbound":
      fields.unshift("_to");
      break;
    default:
      let err = new ArangoError();
      err.errorNum = arangodb.errors.ERROR_BAD_PARAMETER.code;
      err.errorMessage = 'usage: ensureVertexCentricIndex(<...fields>, options), options.direction has to be "inbound" or "outbound"';
      throw err;
  }
  options.fields = fields;
  if (!options.hasOwnProperty('type')) {
    options.type = 'hash';
  }
  return this.ensureIndex(options);
};
