import * as _ from 'underscore';
import { ICategoryFacetRequest } from '../../rest/CategoryFacetRequest';
import { IFacetRequest } from '../../rest/Facet/FacetRequest';
import { IGroupByRequest } from '../../rest/GroupByRequest';
import { IFacetOptions, IQuery, IUserActionsRequest, ICommerceRequest } from '../../rest/Query';
import { IQueryFunction } from '../../rest/QueryFunction';
import { IRankingFunction } from '../../rest/RankingFunction';
import { Utils } from '../../utils/Utils';
import { ExpressionBuilder } from './ExpressionBuilder';
import { QueryBuilderExpression } from './QueryBuilderExpression';

/**
 * The QueryBuilder is used to build a {@link IQuery} that will be able to be executed using the Search API.
 *
 * The class exposes several members and methods that help components and external code to build up the final query that is sent to the Search API.
 *
 */
export class QueryBuilder {
  /**
   * Used to build the basic part of the query expression.
   *
   * This part typically consists of user-entered content such as query keywords, etc.
   * @type {Coveo.ExpressionBuilder}
   */
  public expression: ExpressionBuilder = new ExpressionBuilder();
  /**
   * Used to build the advanced part of the query expression.
   *
   * This part is typically formed of filter expressions generated by components such as facets, external code, etc.
   * @type {Coveo.ExpressionBuilder}
   */
  public advancedExpression: ExpressionBuilder = new ExpressionBuilder();
  /**
   * Used to build the advanced part of the query expression.
   *
   * This part is similar to `advancedExpression`, but its content is interpreted as a constant expression by the index and it takes advantage of special caching features.
   * @type {Coveo.ExpressionBuilder}
   */
  public constantExpression: ExpressionBuilder = new ExpressionBuilder();
  /**
   * The contextual text.
   *
   * This is the contextual text part of the query. It uses the Coveo Machine Learning service to pick key keywords from the text and add them to the basic expression.
   * This field is mainly used to pass context such a case description, long textual query or any other form of text that might help in
   * refining the query.
   */
  public longQueryExpression: ExpressionBuilder = new ExpressionBuilder();
  /**
   * Used to build the disjunctive part of the query expression.
   *
   * When present, this part is evaluated separately from the other expressions and the matching results are merged to those matching expressions, `advancedExpression` and `constantExpression`.
   *
   * The final boolean expression for the query is thus (basic advanced constant) OR (disjunction).
   * @type {Coveo.ExpressionBuilder}
   */
  public disjunctionExpression: ExpressionBuilder = new ExpressionBuilder();
  /**
   * The hub value set from the {@link Analytics} component.
   *
   * Used for analytics reporting in the Coveo platform.
   */
  public searchHub: string;
  /**
   * The tab value set from the {@link Tab} component.
   */
  public tab: string;
  public locale: string;
  /**
   * Name of the query pipeline to use.
   *
   * Specifies the name of the query pipeline to use for the query. If not specified, the default value is default, which means the default query pipeline will be used.
   */
  public pipeline: string;
  /**
   * The maximum age for cached query results, in milliseconds.
   *
   * If results for the exact same request (including user identities) are available in the in-memory cache, they will be used if they are not older than the specified value.
   *
   * Otherwise, the query will be sent to the index.
   */
  public maximumAge: number;

  /**
   * Whether to interpret wildcard characters (`*`) in the basic [`expression`]{@link QueryBuilder.expression} keywords.
   *
   * Setting this attribute to `true` enables the wildcards features of the index, effectively expanding keywords
   * containing wildcard characters (`*`) to the possible matching keywords in order to broaden the query (see
   * [Using Wildcards in Queries](https://docs.coveo.com/en/1580/)).
   *
   * See also [`enableQuestionMarks`]{@link QueryBuilder.enableQuestionMarks}.
   *
   * **Note:**
   * > Normally, the [`enableWildcards`]{@link Querybox.options.enableWildcards} option of the
   * > [`Querybox`]{@link Querybox} component determines the value of this attribute during the initialization of the
   * > search page.
   */
  public enableWildcards: boolean;

  /**
   * Whether to interpret question mark characters (`?`) in the basic [`expression`]{@link QueryBuilder.expression}
   * keywords (see [Using Wildcards in Queries](https://docs.coveo.com/en/1580/).
   *
   * Setting this attribute to `true` has no effect unless [`enableWildcards`]{@link QueryBuilder.enableWildcards} is
   * also `true`.
   *
   * **Note:**
   * > Normally, the [`enableQuestionMarks`]{@link Querybox.options.enableQuestionMarks} option of the
   * > [`Querybox`]{@link Querybox} component determines the value of this attribute during the initialization of the
   * > search page.
   */
  public enableQuestionMarks: boolean;

  /**
   * Whether to interpret special query syntax (e.g., `@objecttype=message`) in the basic
   * [`expression`]{@link QueryBuilder.expression} (see
   * [Coveo Query Syntax Reference](https://docs.coveo.com/en/1552/searching-with-coveo/coveo-cloud-query-syntax)).
   *
   * See also [`enableLowercaseOperators`]{@link QueryBuilder.enableLowercaseOperators}.
   *
   * **Note:**
   * > Normally, the [`enableQuerySyntax`]{@link Querybox.options.enableQuerySyntax} option of the
   * > [`Querybox`]{@link Querybox} component determines the value of this attribute during the initialization of the
   * search page. End user preferences can also modify the value of this attribute.
   *
   * Default value is `false`
   */
  public enableQuerySyntax: boolean = false;

  /**
   * Whether to interpret the `AND`, `NOT`, `OR`, and `NEAR` keywords in the basic
   * [`expression`]{@link QueryBuilder.expression} as query operators, even if those keywords are in lowercase.
   *
   * Setting this attribute to `true` has no effect unless [`enableQuerySyntax`]{@link QueryBuilder.enableQuerySyntax}
   * is also `true`.
   *
   * **Note:**
   * > Normally, the [`enableLowercaseOperators`]{@link Querybox.options.enableLowercaseOperators} option of the
   * > [`Querybox`]{@link Querybox} component determines the value of this attribute during the initialization of the
   * > search page.
   */
  public enableLowercaseOperators: boolean;

  /**
   * Whether to automatically convert the basic [`expression`]{@link QueryBuilder.expression} to a partial match
   * expression if it contains at least a certain number of keywords (see
   * [`partialMatchKeywords`]{@link QueryBuilder.partialMatchKeywords}), so that items containing at least a certain
   * subset of those keywords (see [`partialMatchThreshold`]{@link QueryBuilder.partialMatchThreshold}) will match the
   * query.
   *
   * **Note:**
   * > Normally, the [`enablePartialMatch`]{@link Querybox.options.enablePartialMatch} option of the
   * > [`Querybox`]{@link Querybox} component determines the value of this attribute during the initialization of the
   * > search page.
   */
  public enablePartialMatch: boolean;

  /**
   * The minimum number of keywords that need to be present in the basic [`expression`]{@link QueryBuilder.expression}
   * to convert it to a partial match expression.
   *
   * The value of this attribute has no meaning unless [`enablePartialMatch`]{@link QueryBuilder.enablePartialMatch} is
   * `true`.
   *
   * See also [`partialMatchThreshold`]{@link QueryBuilder.partialMatchThreshold}.
   *
   * **Note:**
   * > Normally, the [`partialMatchKeywords`]{@link Querybox.options.partialMatchKeywords} option of the
   * > [`Querybox`]{@link Querybox} component determines the value of this attribute during the initialization of the
   * > search page.
   */
  public partialMatchKeywords: number;

  /**
   * An absolute or relative (percentage) value indicating the minimum number of partial match expression keywords an
   * item must contain to match the query.
   *
   * The value of this attribute has no meaning unless [`enablePartialMatch`]{@link QueryBuilder.enablePartialMatch} is
   * `true`.
   *
   * See also [`partialMatchKeywords`]{@link QueryBuilder.partialMatchKeywords}.
   *
   * **Note:**
   * > Normally, the [`partialMatchThreshold`]{@link Querybox.options.partialMatchThreshold} option of the
   * > [`Querybox`]{@link Querybox} component determines the value of this attribute during the initialization of the
   * > search page.
   */
  public partialMatchThreshold: string;
  /**
   * This is the 0-based index of the first result to return.
   *
   * If not specified, this parameter defaults to 0.
   */
  public firstResult: number = 0;
  /**
   * This is the number of results to return, starting from {@link IQuery.firstResult}.
   *
   * If not specified, this parameter defaults to 10.
   */
  public numberOfResults: number = 10;
  /**
   * Specifies the length (in number of characters) of the excerpts generated by the indexer based on the keywords present in the query.
   *
   * The index includes the top most interesting sentences (in the order they appear in the item) that fit in the specified number of characters.
   *
   * When not specified, the default value is 200.
   */
  public excerptLength: number;
  /**
   * Specifies a field on which {@link Folding} should be performed.
   *
   * Folding is a kind of duplicate filtering where only the first result with any given value of the field is included in the result set.
   *
   * It's typically used to return only one result in a conversation, for example when forum posts in a thread are indexed as separate items.
   */
  public filterField: string;
  /**
   * Number of results that should be folded, using the {@link IQuery.filterField}.
   */
  public filterFieldRange: number;
  /**
   * Specifies the `parentField` when doing parent-child loading (See: {@link Folding}).
   */
  public parentField: string;
  /**
   * Specifies the childField when doing parent-child loading (See: {@link Folding}).
   */
  public childField: string;
  public fieldsToInclude: string[];
  public requiredFields: string[] = [];
  public includeRequiredFields: boolean = false;
  public fieldsToExclude: string[];
  /**
   * Whether to enable query corrections on this query (see {@link DidYouMean}).
   */
  public enableDidYouMean: boolean = false;
  /**
   * Whether to enable debug info on the query.
   *
   * This will return additional information on the resulting JSON response from the Search API.
   *
   * Mostly: execution report (a detailed breakdown of the parsed and executed query).
   */
  public enableDebug: boolean = false;
  /**
   * **Note:**
   *
   * > The Coveo Cloud V2 platform does not support collaborative rating. Therefore, this property is obsolete in Coveo Cloud V2.
   *
   * Whether the index should take collaborative rating in account when ranking result (See: {@link ResultRating}).
   */
  public enableCollaborativeRating: boolean;
  /**
   * Specifies the sort criterion(s) to use to sort results. If not specified, this parameter defaults to relevancy.
   *
   * Possible values are : <br/>
   * -- relevancy :  This uses all the configured ranking weights as well as any specified ranking expressions to rank results.<br/>
   * -- dateascending / datedescending Sort using the value of the `@date` field, which is typically the last modification date of an item in the index.<br/>
   * -- qre : Sort using only the weights applied through ranking expressions. This is much like using `relevancy` except that automatic weights based on keyword proximity etc, are not computed.<br/>
   * -- nosort : Do not sort the results. The order in which items are returned is essentially random.<br/>
   * -- @field ascending / @field descending Sort using the value of a custom field.
   */
  public sortCriteria: string = 'relevancy';
  /**
   * @deprecated
   */
  public sortField: string;
  public retrieveFirstSentences: boolean = true;
  public timezone: string;
  public queryUid: string;
  /**
   * Specifies an array of Query Function operation that will be executed on the results.
   */
  public queryFunctions: IQueryFunction[] = [];
  /**
   * Specifies an array of Ranking Function operations that will be executed on the results.
   */
  public rankingFunctions: IRankingFunction[] = [];
  /**
   * Specifies an array of Group By operations that can be performed on the query results to extract facets.
   * Cannot be used alongside [`facetRequests`]{@link QueryBuilder.facetRequests}
   */
  public groupByRequests: IGroupByRequest[] = [];

  /**
   * Specifies an array of request for the DynamicFacet component.
   * Cannot be used alongside [`groupByRequests`]{@link QueryBuilder.groupByRequests}
   */
  public facetRequests: IFacetRequest[] = [];

  /**
   * The global configuration options to apply to the requests in the [facets]{@link QueryBuilder.facets} array.
   */
  public facetOptions: IFacetOptions = {};

  /**
   * Specifies an array of request for the CategoryFacet component.
   */
  public categoryFacets: ICategoryFacetRequest[] = [];
  public enableDuplicateFiltering: boolean = false;
  /**
   * The custom context information to send along with the query. Each value should be a string or an array of strings.
   *
   * Custom context information can influence the output of Coveo Machine Learning models and can also be used in query pipeline conditions.
   */
  public context: { [key: string]: any };
  /**
   * The actions history represents the past actions a user made and is used by the Coveo Machine Learning service to suggest recommendations.
   * It is generated by the page view script (https://github.com/coveo/coveo.analytics.js).
   */
  public actionsHistory: string;
  /**
   * This is the ID of the recommendation interface that generated the query.
   */
  public recommendation: string;
  /**
   * Specifies if the Search API should perform queries even when no keywords were entered by the end user.
   *
   * End user keywords are present in either the {@link IQuery.q} or {@link IQuery.lq} part of the query.
   *
   * This parameter is normally controlled by {@link SearchInterface.options.allowEmptyQuery} option.
   */
  public allowQueriesWithoutKeywords: boolean;
  /**
   * A request to retrieve user actions in the query response.
   */
  public userActions: IUserActionsRequest;
  /**
   * A request for a commerce query.
   */
  public commerce: ICommerceRequest;
  /**
   * Build the current content or state of the query builder and return a {@link IQuery}.
   *
   * build can be called multiple times on the same QueryBuilder.
   * @returns {IQuery}
   */
  public build(): IQuery {
    const query: IQuery = {
      q: this.expression.build(),
      aq: this.advancedExpression.build(),
      cq: this.constantExpression.build(),
      lq: this.longQueryExpression.build(),
      dq: this.disjunctionExpression.build(),

      searchHub: this.searchHub,
      tab: this.tab,
      locale: this.locale,
      pipeline: this.pipeline,
      maximumAge: this.maximumAge,

      wildcards: this.enableWildcards,
      questionMark: this.enableQuestionMarks,
      lowercaseOperators: this.enableLowercaseOperators,
      partialMatch: this.enablePartialMatch,
      partialMatchKeywords: this.partialMatchKeywords,
      partialMatchThreshold: this.partialMatchThreshold,

      firstResult: this.firstResult,
      numberOfResults: this.numberOfResults,
      excerptLength: this.excerptLength,
      filterField: this.filterField,
      filterFieldRange: this.filterFieldRange,
      parentField: this.parentField,
      childField: this.childField,
      fieldsToInclude: this.computeFieldsToInclude(),
      fieldsToExclude: this.fieldsToExclude,
      enableDidYouMean: this.enableDidYouMean,
      sortCriteria: this.sortCriteria,
      sortField: this.sortField,
      queryFunctions: this.queryFunctions,
      rankingFunctions: this.rankingFunctions,
      groupBy: this.groupBy,
      facets: this.facets,
      facetOptions: this.facetOptions,
      categoryFacets: this.categoryFacets,
      retrieveFirstSentences: this.retrieveFirstSentences,
      timezone: this.timezone,
      enableQuerySyntax: this.enableQuerySyntax,
      enableDuplicateFiltering: this.enableDuplicateFiltering,
      enableCollaborativeRating: this.enableCollaborativeRating,
      debug: this.enableDebug,
      context: this.context,
      actionsHistory: this.actionsHistory,
      recommendation: this.recommendation,
      allowQueriesWithoutKeywords: this.allowQueriesWithoutKeywords,
      userActions: this.userActions,
      commerce: this.commerce
    };
    return query;
  }

  /**
   * Return only the expression(s) part(s) of the query, as a string.
   *
   * This means the basic, advanced and constant part in a complete expression {@link IQuery.q}, {@link IQuery.aq}, {@link IQuery.cq}.
   * @returns {string}
   */
  public computeCompleteExpression(): string {
    return this.computeCompleteExpressionParts().full;
  }

  /**
   * Return only the expression(s) part(s) of the query, as an object.
   * @returns {{full: string, withoutConstant: string, constant: string}}
   */
  public computeCompleteExpressionParts(): QueryBuilderExpression {
    return new QueryBuilderExpression(
      this.expression.build(),
      this.advancedExpression.build(),
      this.constantExpression.build(),
      this.disjunctionExpression.build()
    );
  }

  /**
   * Return only the expression(s) part(s) of the query, as a string, except the given expression.
   *
   * This is used by {@link Facet}, to build their group by request with query override.
   * @param except
   * @returns {string}
   */
  public computeCompleteExpressionExcept(except: string): string {
    return this.computeCompleteExpressionPartsExcept(except).full;
  }

  /**
   * Return only the expression(s) part(s) of the query, as an object, except the given expression.
   *
   * This is used by {@link Facet}, to build their group by request with query override.
   * @param except
   * @returns {{full: string, withoutConstant: string, constant: string}}
   */
  public computeCompleteExpressionPartsExcept(except: string): QueryBuilderExpression {
    const withoutConstantAndExcept = ExpressionBuilder.merge(this.expression, this.advancedExpression);
    withoutConstantAndExcept.remove(except);

    const basicWithoutException = new ExpressionBuilder();
    basicWithoutException.fromExpressionBuilder(this.expression);
    basicWithoutException.remove(except);

    const advancedWithoutException = new ExpressionBuilder();
    advancedWithoutException.fromExpressionBuilder(this.advancedExpression);
    advancedWithoutException.remove(except);

    const constantWithoutException = new ExpressionBuilder();
    constantWithoutException.fromExpressionBuilder(this.constantExpression);
    constantWithoutException.remove(except);

    const disjunctionWithoutException = new ExpressionBuilder();
    disjunctionWithoutException.fromExpressionBuilder(this.disjunctionExpression);
    disjunctionWithoutException.remove(except);

    return new QueryBuilderExpression(
      basicWithoutException.build(),
      advancedWithoutException.build(),
      constantWithoutException.build(),
      disjunctionWithoutException.build()
    );
  }

  /**
   * Add fields to specifically include when the results return.
   *
   * This can be used to accelerate the execution time of every query, as there is much less data to process if you whitelist specific fields.
   * @param fields
   */
  public addFieldsToInclude(fields: string[]) {
    this.fieldsToInclude = _.uniq((this.fieldsToInclude || []).concat(fields));
  }

  public addRequiredFields(fields: string[]) {
    this.requiredFields = _.uniq(this.requiredFields.concat(fields));
  }

  /**
   * Add fields to specifically exclude when the results return.
   *
   * This can be used to accelerate the execution time of every query, as there is much less data to process if you blacklist specific fields.
   * @param fields
   */
  public addFieldsToExclude(fields: string[]) {
    this.fieldsToExclude = _.uniq((this.fieldsToInclude || []).concat(fields));
  }

  public computeFieldsToInclude() {
    if (this.includeRequiredFields || this.fieldsToInclude != null) {
      return _.uniq(this.requiredFields.concat(this.fieldsToInclude || []));
    } else {
      return null;
    }
  }

  /**
   * Adds or updates a single context key-value pair in the `context` object.
   *
   * @param key The context key. If this key is already present in the `context` object, its value is updated.
   * @param value The context value. This should be a string or an array of strings.
   */
  public addContextValue(key: string, value: any) {
    if (this.context == null) {
      this.context = {};
    }
    this.context[key] = value;
  }

  /**
   * Merges the specified `values` into the `context` object.
   *
   * @param values The object to merge into the `context` object. Can contain multiple key-value pairs, where each value should be a string or an array of strings. If some keys are already present in the `context` object, their values are updated.
   */
  public addContext(values: { [key: string]: any }) {
    if (this.context == null) {
      this.context = {};
    }
    _.extend(this.context, values);
  }

  /**
   * Returns true if the current query contains any expression that are considered "end user input".
   *
   * This usually means anything entered in the basic (see [q]{@link IQuery.options.q}) or long (see [lq]{@link IQuery.options.lq}) part of the query.
   */
  public containsEndUserKeywords(): boolean {
    const query = this.build();
    return Utils.isNonEmptyString(query.q) || Utils.isNonEmptyString(query.lq);
  }

  private get groupBy() {
    if (Utils.isEmptyArray(this.groupByRequests)) {
      return undefined;
    }

    return this.groupByRequests;
  }

  private set groupBy(groupBy: IGroupByRequest[]) {
    this.groupByRequests = groupBy;
  }

  private get facets() {
    if (Utils.isEmptyArray(this.facetRequests)) {
      return undefined;
    }

    return this.facetRequests;
  }

  private set facets(facets: IFacetRequest[]) {
    this.facetRequests = facets;
  }
}
