/**
 * @class MLSearchController
 * @classdesc base search controller class; the prototype for an angular search controller
 *
 * Note: this style requires you to use the `controllerAs` syntax.
 *
 * <pre class="prettyprint">
 *   (function() {
 *     'use strict';
 *
 *     angular.module('app').controller('SearchCtrl', SearchCtrl);
 *
 *     SearchCtrl.$inject = ['$scope', '$location', 'MLSearchFactory'];
 *
 *     // inherit from MLSearchController
 *     var superCtrl = MLSearchController.prototype;
 *     SearchCtrl.prototype = Object.create(superCtrl);
 *
 *     function SearchCtrl($scope, $location, searchFactory) {
 *       var ctrl = this;
 *       var mlSearch = searchFactory.newContext();
 *
 *       MLSearchController.call(ctrl, $scope, $location, mlSearch);
 *
 *       // override a superCtrl method
 *       ctrl.updateSearchResults = function updateSearchResults(data) {
 *         superCtrl.updateSearchResults.apply(ctrl, arguments);
 *         console.log('updated search results');
 *       }
 *
 *       ctrl.init();
 *     }
 *   })();
 * </pre>
 *
 * @param {Object} $scope - child controller's scope
 * @param {Object} $location - angular's $location service
 * @param {MLSearchContext} mlSearch - child controller's searchContext
 *
 * @prop {Object} $scope - child controller's scope
 * @prop {Object} $location - angular's $location service
 * @prop {MLSearchContext} mlSearch - child controller's searchContext
 * @prop {Boolean} searchPending - signifies whether a search is in progress
 * @prop {Number} page - the current results page
 * @prop {String} qtext - the current query text
 * @prop {Object} response - the search response object
 */

function MLSearchController($scope, $location, mlSearch) {
  'use strict';
  if ( !(this instanceof MLSearchController) ) {
    return new MLSearchController($scope, $location, mlSearch);
  }

  // TODO: error if not passed
  this.$scope = $scope;
  this.$location = $location;
  this.mlSearch = mlSearch;

  this.searchPending = false;
  this.page = 1;
  this.qtext = '';
  this.response = {};
}

/**
 * <strong>UNIMPLEMENTED EXTENSION METHOD</strong>
 *
 * implement to support extra URL params that can trigger a search;
 *
 * should read extra URL params, and update the controller state
 *
 * @method MLSearchController#parseExtraURLParams
 * @return {Boolean} should a search be triggered
 */

/**
 * <strong>UNIMPLEMENTED EXTENSION METHOD</strong>
 *
 * implement to support additional URL params that can trigger a search;
 *
 * should update extra URL params from the controller state
 *
 * @method MLSearchController#updateExtraURLParams
 */

(function() {
  'use strict';

  /**
   * initialize the controller, setting the search state form URL params,
   * and creating a handler for the `$locationChangeSuccess` event
   *
   * @memberof MLSearchController
   * @return {Promise} the promise from {@link MLSearchContext#fromParams}
   */
  MLSearchController.prototype.init = function init() {
    // monitor URL params changes (forward/back, etc.)
    this.$scope.$on('$locationChangeSuccess', this.locationChange.bind(this));

    // capture initial URL params in mlSearch and ctrl
    if ( this.parseExtraURLParams ) {
      this.parseExtraURLParams();
    }

    return this.mlSearch.fromParams()
      .then( this._search.bind(this) );
  };

  /**
   * handle the `$locationChangeSuccess` event
   *
   * checks if mlSearch URL params or additional params have changed
   * (using the child controller's `parseExtraURLParams()` method, if available),
   * and, if necessary, initiates a search via {@link MLSearchController#_search}
   *
   * @memberof MLSearchController
   * @param {Object} e - the `$locationChangeSuccess` event object
   * @param {String} newUrl
   * @param {String} oldUrl
   * @return {Promise} the promise from {@link MLSearchContext#locationChange}
   */
  MLSearchController.prototype.locationChange = function locationChange(e, newUrl, oldUrl) {
    var self = this,
        shouldUpdate = false;

    if ( this.parseExtraURLParams ) {
      shouldUpdate = this.parseExtraURLParams();
    }

    return this.mlSearch.locationChange( newUrl, oldUrl )
      .then(
        this._search.bind(this),
        function() {
          if (shouldUpdate) {
            self._search();
          }
        }
      );
  };

  /**
   * search implementation function
   *
   * sets {@link MLSearchController#searchPending} to `true`,
   * invokes {@link MLSearchContext#search} with {@link MLSearchController#updateSearchResults} as the callback,
   * and invokes {@link MLSearchController#updateURLParams}
   *
   * @memberof MLSearchController
   * @return {Promise} the promise from {@link MLSearchContext#search}
   */
  MLSearchController.prototype._search = function _search() {
    this.searchPending = true;

    var promise = this.mlSearch.search()
      .then( this.updateSearchResults.bind(this) );

    this.updateURLParams();
    return promise;
  };

  /**
   * updates controller state with search results
   *
   * sets {@link MLSearchController#searchPending} to `true`,
   * sets {@link MLSearchController#response}, {@link MLSearchController#qtext},
   * and {@link MLSearchController#page} to values from the response
   *
   * @memberof MLSearchController
   * @param {Object} data - the response from {@link MLSearchContext#search}
   * @return {MLSearchController} `this`
   */
  MLSearchController.prototype.updateSearchResults = function updateSearchResults(data) {
    this.searchPending = false;
    this.response = data;
    this.qtext = this.mlSearch.getText();
    this.page = this.mlSearch.getPage();
    return this;
  };

  /**
   * updates URL params based on the current {@link MLSearchContext} state, preserving any additional params.
   * invokes the child controller's `updateExtraURLParams()` method, if available
   *
   * @memberof MLSearchController
   * @return {MLSearchController} `this`
   */
  MLSearchController.prototype.updateURLParams = function updateURLParams() {
    var params = _.chain( this.$location.search() )
      .omit( this.mlSearch.getParamsKeys() )
      .merge( this.mlSearch.getParams() )
      .value();

    this.$location.search( params );

    if ( this.updateExtraURLParams ) {
      this.updateExtraURLParams();
    }
    return this;
  };

  /**
   * the primary search method, for use with any user-triggered searches (for instance, from an input control)
   *
   * @memberof MLSearchController
   * @param {String} [qtext] - if present, updates the state of {@link MLSearchController#qtext}
   * @return {Promise} the promise from {@link MLSearchController#_search}
   */
  MLSearchController.prototype.search = function search(qtext) {
    if ( arguments.length ) {
      this.qtext = qtext;
    }

    this.mlSearch.setText( this.qtext ).setPage( this.page );
    return this._search();
  };

  /**
   * clear qtext, facet selections, boost queries, and additional queries. Then, run a search.
   *
   * @memberof MLSearchController
   * @return {Promise} the promise from {@link MLSearchController#_search}
   */
  MLSearchController.prototype.reset = function reset() {
    this.mlSearch
      .clearAllFacets()
      .clearAdditionalQueries()
      .clearBoostQueries();
    this.qtext = '';
    this.page = 1;
    return this._search();
  };

  /**
   * toggle the selection state of the specified facet value
   *
   * @memberof MLSearchController
   * @param {String} facetName - the name of the facet to toggle
   * @param {String} value - the value of the facet to toggle
   * @return {Promise} the promise from {@link MLSearchController#_search}
   */
  MLSearchController.prototype.toggleFacet = function toggleFacet(facetName, value) {
    this.mlSearch.toggleFacet( facetName, value );
    return this._search();
  };

  /**
   * toggle the selection state of the specified NEGATED facet value
   *
   * @memberof MLSearchController
   * @param {String} facetName - the name of the NEGATED facet to toggle
   * @param {String} value - the value of the NEGATED facet to toggle
   * @return {Promise} the promise from {@link MLSearchController#_search}
   */
  MLSearchController.prototype.toggleNegatedFacet = function toggleNegatedFacet(facetName, value) {
    this.mlSearch.toggleFacet( facetName, value, true );
    return this._search();
  };

  /**
   * Appends additional facet values to the provided facet object.
   *
   * @memberof MLSearchController
   * @param {Object} facet - a facet object from {@link MLSearchController#response}
   * @param {String} facetName - facet name
   * @param {Number} [step] - the number of additional facet values to retrieve (defaults to `5`)
   * @return {Promise} the promise from {@link MLSearchContext#showMoreFacets}
   */
  MLSearchController.prototype.showMoreFacets = function showMoreFacets(facet, facetName, step) {
    return this.mlSearch.showMoreFacets(facet, facetName, step);
  };

  /**
   * clear all facet selections, and run a search
   *
   * @memberof MLSearchController
   * @return {Promise} the promise from {@link MLSearchController#_search}
   */
  MLSearchController.prototype.clearFacets = function clearFacets() {
    this.mlSearch.clearAllFacets();
    return this._search();
  };

  /**
   * Gets search phrase suggestions based on the current state.
   * This method can be passed directly to the ui-bootstrap `typeahead` directive.
   *
   * @memberof MLSearchController
   * @param {String} qtext - the partial-phrase to match
   * @param {String|Object} [options] - string options name (to override suggestOptions), or object for adhoc combined query options
   * @return {Promise} the promise from {@link MLSearchContext#suggest}
   */
  MLSearchController.prototype.suggest = function suggest(qtext, options) {
    return this.mlSearch.suggest(qtext, options).then(function(res) {
      return res.suggestions || [];
    });
  };
})();