# search.js

    const _ = require('underscore');
    const AVRequest = require('./request')._request;

    module.exports = function(AV) {
      /**
       * A builder to generate sort string for app searching.For example:
       * @class
       * @since 0.5.1
       * @example
       *   var builder = new AV.SearchSortBuilder();
       *   builder.ascending('key1').descending('key2','max');
       *   var query = new AV.SearchQuery('Player');
       *   query.sortBy(builder);
       *   query.find().then();
       */
      AV.SearchSortBuilder = function() {
        this._sortFields = [];
      };

      _.extend(
        AV.SearchSortBuilder.prototype,
        /** @lends AV.SearchSortBuilder.prototype */ {
          _addField: function(key, order, mode, missing) {
            var field = {};
            field[key] = {
              order: order || 'asc',
              mode: mode || 'avg',
              missing: '_' + (missing || 'last'),
            };
            this._sortFields.push(field);
            return this;
          },

          /**
           * Sorts the results in ascending order by the given key and options.
           *
           * @param {String} key The key to order by.
           * @param {String} mode The sort mode, default is 'avg', you can choose
           *                  'max' or 'min' too.
           * @param {String} missing The missing key behaviour, default is 'last',
           *                  you can choose 'first' too.
           * @return {AV.SearchSortBuilder} Returns the builder, so you can chain this call.
           */
          ascending: function(key, mode, missing) {
            return this._addField(key, 'asc', mode, missing);
          },

          /**
           * Sorts the results in descending order by the given key and options.
           *
           * @param {String} key The key to order by.
           * @param {String} mode The sort mode, default is 'avg', you can choose
           *                  'max' or 'min' too.
           * @param {String} missing The missing key behaviour, default is 'last',
           *                  you can choose 'first' too.
           * @return {AV.SearchSortBuilder} Returns the builder, so you can chain this call.
           */
          descending: function(key, mode, missing) {
            return this._addField(key, 'desc', mode, missing);
          },

          /**
           * Add a proximity based constraint for finding objects with key point
           * values near the point given.
           * @param {String} key The key that the AV.GeoPoint is stored in.
           * @param {AV.GeoPoint} point The reference AV.GeoPoint that is used.
           * @param {Object} options The other options such as mode,order, unit etc.
           * @return {AV.SearchSortBuilder} Returns the builder, so you can chain this call.
           */
          whereNear: function(key, point, options) {
            options = options || {};
            var field = {};
            var geo = {
              lat: point.latitude,
              lon: point.longitude,
            };
            var m = {
              order: options.order || 'asc',
              mode: options.mode || 'avg',
              unit: options.unit || 'km',
            };
            m[key] = geo;
            field['_geo_distance'] = m;

            this._sortFields.push(field);
            return this;
          },

          /**
           * Build a sort string by configuration.
           * @return {String} the sort string.
           */
          build: function() {
            return JSON.stringify(AV._encode(this._sortFields));
          },
        }
      );

      /**
       * App searching query.Use just like AV.Query:
       *
       * Visit <a href='https://leancloud.cn/docs/app_search_guide.html'>App Searching Guide</a>
       * for more details.
       * @class
       * @since 0.5.1
       * @example
       *   var query = new AV.SearchQuery('Player');
       *   query.queryString('*');
       *   query.find().then(function(results) {
       *     console.log('Found %d objects', query.hits());
       *     //Process results
       *   });
       */
      AV.SearchQuery = AV.Query._extend(
        /** @lends AV.SearchQuery.prototype */ {
          _sid: null,
          _hits: 0,
          _queryString: null,
          _highlights: null,
          _sortBuilder: null,
          _createRequest: function(params, options) {
            return AVRequest(
              'search/select',
              null,
              null,
              'GET',
              params || this.toJSON(),
              options
            );
          },

          /**
           * Sets the sid of app searching query.Default is null.
           * @param {String} sid  Scroll id for searching.
           * @return {AV.SearchQuery} Returns the query, so you can chain this call.
           */
          sid: function(sid) {
            this._sid = sid;
            return this;
          },

          /**
           * Sets the query string of app searching.
           * @param {String} q  The query string.
           * @return {AV.SearchQuery} Returns the query, so you can chain this call.
           */
          queryString: function(q) {
            this._queryString = q;
            return this;
          },

          /**
           * Sets the highlight fields. Such as
           * <pre><code>
           *   query.highlights('title');
           *   //or pass an array.
           *   query.highlights(['title', 'content'])
           * </code></pre>
           * @param {String[]} highlights a list of fields.
           * @return {AV.SearchQuery} Returns the query, so you can chain this call.
           */
          highlights: function(highlights) {
            var objects;
            if (highlights && _.isString(highlights)) {
              objects = arguments;
            } else {
              objects = highlights;
            }
            this._highlights = objects;
            return this;
          },

          /**
           * Sets the sort builder for this query.
           * @see AV.SearchSortBuilder
           * @param { AV.SearchSortBuilder} builder The sort builder.
           * @return {AV.SearchQuery} Returns the query, so you can chain this call.
           *
           */
          sortBy: function(builder) {
            this._sortBuilder = builder;
            return this;
          },

          /**
           * Returns the number of objects that match this query.
           * @return {Number}
           */
          hits: function() {
            if (!this._hits) {
              this._hits = 0;
            }
            return this._hits;
          },

          _processResult: function(json) {
            delete json['className'];
            delete json['_app_url'];
            delete json['_deeplink'];
            return json;
          },

          /**
           * Returns true when there are more documents can be retrieved by this
           * query instance, you can call find function to get more results.
           * @see AV.SearchQuery#find
           * @return {Boolean}
           */
          hasMore: function() {
            return !this._hitEnd;
          },

          /**
           * Reset current query instance state(such as sid, hits etc) except params
           * for a new searching. After resetting, hasMore() will return true.
           */
          reset: function() {
            this._hitEnd = false;
            this._sid = null;
            this._hits = 0;
          },

          /**
           * Retrieves a list of AVObjects that satisfy this query.
           * Either options.success or options.error is called when the find
           * completes.
           *
           * @see AV.Query#find
           * @return {AV.Promise} A promise that is resolved with the results when
           * the query completes.
           */
          find: function() {
            var self = this;

            var request = this._createRequest();

            return request.then(function(response) {
              //update sid for next querying.
              if (response.sid) {
                self._oldSid = self._sid;
                self._sid = response.sid;
              } else {
                self._sid = null;
                self._hitEnd = true;
              }
              self._hits = response.hits || 0;

              return _.map(response.results, function(json) {
                if (json.className) {
                  response.className = json.className;
                }
                var obj = self._newObject(response);
                obj.appURL = json['_app_url'];
                obj._finishFetch(self._processResult(json), true);
                return obj;
              });
            });
          },

          toJSON: function() {
            var params = AV.SearchQuery.__super__.toJSON.call(this);
            delete params.where;
            if (this.className) {
              params.clazz = this.className;
            }
            if (this._sid) {
              params.sid = this._sid;
            }
            if (!this._queryString) {
              throw new Error('Please set query string.');
            } else {
              params.q = this._queryString;
            }
            if (this._highlights) {
              params.highlights = this._highlights.join(',');
            }
            if (this._sortBuilder && params.order) {
              throw new Error('sort and order can not be set at same time.');
            }
            if (this._sortBuilder) {
              params.sort = this._sortBuilder.build();
            }

            return params;
          },
        }
      );
    };