import 'styling/_Querybox';
import { IBuildingQueryEventArgs, QueryEvents } from '../../events/QueryEvents';
import { StandaloneSearchInterfaceEvents } from '../../events/StandaloneSearchInterfaceEvents';
import { exportGlobally } from '../../GlobalExports';
import { Grammar } from '../../magicbox/Grammar';
import { createMagicBox, MagicBoxInstance } from '../../magicbox/MagicBox';
import { Result } from '../../magicbox/Result/Result';
import { Assert } from '../../misc/Assert';
import { IAttributeChangedEventArg, MODEL_EVENTS } from '../../models/Model';
import { QueryStateModel, QUERY_STATE_ATTRIBUTES } from '../../models/QueryStateModel';
import { l } from '../../strings/Strings';
import { $$ } from '../../utils/Dom';
import { analyticsActionCauseList, IAnalyticsNoMeta } from '../Analytics/AnalyticsActionListMeta';
import { Component } from '../Base/Component';
import { IComponentBindings } from '../Base/ComponentBindings';
import { ComponentOptions } from '../Base/ComponentOptions';
import { Initialization } from '../Base/Initialization';
import { QueryboxOptionsProcessing } from './QueryboxOptionsProcessing';
import { QueryboxQueryParameters } from './QueryboxQueryParameters';

export interface IQueryboxOptions {
  enableSearchAsYouType?: boolean;
  searchAsYouTypeDelay?: number;
  enableQuerySyntax?: boolean;
  enableWildcards?: boolean;
  enableQuestionMarks?: boolean;
  enableLowercaseOperators?: boolean;
  enablePartialMatch?: boolean;
  partialMatchKeywords?: number;
  partialMatchThreshold?: string;
  placeholder?: string;
  triggerQueryOnClear?: boolean;
}

/**
 * The `Querybox` component renders an input which the end user can interact with to enter and submit queries.
 *
 * When the end user submits a search request, the `Querybox` component triggers a query and logs the corresponding
 * usage analytics data.
 *
 * For technical reasons, it is necessary to instantiate this component on a `div` element rather than on an `input`
 * element.
 *
 * See also the [`Searchbox`]{@link Searchbox} component, which can automatically instantiate a `Querybox` along with an
 * optional [`SearchButton`]{@link SearchButton} component.
 */
export class Querybox extends Component {
  static ID = 'Querybox';

  static doExport = () => {
    exportGlobally({
      Querybox: Querybox,
      QueryboxQueryParameters: QueryboxQueryParameters
    });
  };

  /**
   * The options for the Querybox.
   * @componentOptions
   */
  public static options: IQueryboxOptions = {
    /**
     * Whether to enable the search-as-you-type feature.
     *
     * **Note:** Enabling this feature can consume lots of queries per month (QPM), especially if the [`searchAsYouTypeDelay`]{@link Querybox.options.searchAsYouTypeDelay} option is set to a low value.
     *
     * Default value is `false`.
     */
    enableSearchAsYouType: ComponentOptions.buildBooleanOption({ defaultValue: false, section: 'Advanced Options' }),

    /**
     * If the [`enableSearchAsYouType`]{@link Querybox.options.enableSearchAsYouType} option is `true`, specifies how
     * long to wait (in milliseconds) between each key press before triggering a new query.
     *
     * Default value is `50`. Minimum value is `0`
     */
    searchAsYouTypeDelay: ComponentOptions.buildNumberOption({ defaultValue: 50, min: 0, section: 'Advanced Options' }),

    /**
     * Specifies whether to interpret special query syntax (e.g., `@objecttype=message`) when the end user types
     * a query in the `Querybox` (see
     * [Coveo Query Syntax Reference](https://docs.coveo.com/en/1552/searching-with-coveo/coveo-cloud-query-syntax)). Setting this
     * option to `true` also causes the `Querybox` to highlight any query syntax.
     *
     * Regardless of the value of this option, the Coveo Cloud REST Search API always interprets expressions surrounded
     * by double quotes (`"`) as exact phrase match requests.
     *
     * See also [`enableLowercaseOperators`]{@link Querybox.options.enableLowercaseOperators}.
     *
     * **Notes:**
     * > * End user preferences can override the value you specify for this option.
     * >
     * > If the end user selects a value other than **Automatic** for the **Enable query syntax** setting (see
     * > the [`enableQuerySyntax`]{@link ResultsPreferences.options.enableQuerySyntax} option of the
     * > [`ResultsPreferences`]{@link ResultsPreferences} component), the end user preference takes precedence over this
     * > option.
     * >
     * > * On-premises versions of the Coveo Search API require this option to be set to `true` in order to interpret
     * > expressions surrounded by double quotes (`"`) as exact phrase match requests.
     *
     * Default value is `false`.
     */
    enableQuerySyntax: ComponentOptions.buildBooleanOption({ defaultValue: false, section: 'Advanced Options' }),

    /**
     * Specifies whether to expand basic expression keywords containing wildcards 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 Querybox.options.enableQuestionMarks}.
     *
     *  **Note:**
     * > If you are using an on-premises version of the Coveo Search API, you need to set the
     * > [`enableQuerySyntax`]{@link Querybox.options.enableQuerySyntax} option to `true` to be able to set
     * > `enableWildcards` to `true`.
     *
     * Default value is `false`.
     */
    enableWildcards: ComponentOptions.buildBooleanOption({ defaultValue: false, section: 'Advanced Options' }),

    /**
     * If [`enableWildcards`]{@link Querybox.options.enableWildcards} is `true`, specifies whether to expand basic
     * expression keywords containing question mark characters (`?`) to the possible matching keywords in order to
     * broaden the query (see
     * [Using Wildcards in Queries](https://docs.coveo.com/en/1580/)).
     *
     * **Note:**
     * > If you are using an on-premises version of the Coveo Search API, you also need to set the
     * > [`enableQuerySyntax`]{@link Querybox.options.enableQuerySyntax} option to `true` in order to be able to set
     * > `enableQuestionMarks` to `true`.
     *
     * Default value is `false`.
     */
    enableQuestionMarks: ComponentOptions.buildBooleanOption({ defaultValue: false, depend: 'enableWildcards' }),

    /**
     * If the [`enableQuerySyntax`]{@link Querybox.options.enableQuerySyntax} option is `true`, specifies whether to
     * interpret the `AND`, `NOT`, `OR`, and `NEAR` keywords in the `Querybox` as query operators in the query, even if
     * the end user types those keywords in lowercase.
     *
     * This option applies to all query operators (see
     * [Coveo Query Syntax Reference](https://docs.coveo.com/en/1552/searching-with-coveo/coveo-cloud-query-syntax)).
     *
     * **Example:**
     * > If this option and the `enableQuerySyntax` option are both `true`, the Coveo Platform interprets the `near`
     * > keyword in a query such as `service center near me` as the `NEAR` query operator (not as a query term).
     *
     * > Otherwise, if the `enableQuerySyntax` option is `true` and this option is `false`, the end user has to type the
     * > `NEAR` keyword in uppercase for the Coveo Platform to interpret it as a query operator.
     *
     * Default value is `false`.
     */
    enableLowercaseOperators: ComponentOptions.buildBooleanOption({ defaultValue: false, depend: 'enableQuerySyntax' }),

    /**
     * Whether to convert a basic expression containing at least a certain number of keywords (see the
     * [`partialMatchKeywords`]{@link Querybox.options.partialMatchKeywords} option) to *partial match expression*, so
     * that items containing at least a certain number of those keywords (see the
     * [`partialMatchThreshold`]{@link Querybox.options.partialMatchThreshold} option) will match the expression.
     *
     * **Notes:**
     * - Only the basic expression of the query (see [`q`]{@link q}) can be converted to a partial match expression.
     * - When the [`enableQuerySyntax`]{@link Querybox.options.enableQuerySyntax} option is set to `true`, this feature has no effect if the basic expression contains advanced query syntax (field expressions, operators, etc.).
     *
     * @notSupportedIn salesforcefree
     */
    enablePartialMatch: ComponentOptions.buildBooleanOption({ defaultValue: false }),

    /**
     * The minimum number of keywords that need to be present in the basic expression to convert it to a partial match expression.
     *
     * See also the [`partialMatchThreshold`]{@link Querybox.options.partialMatchThreshold} option.
     *
     * **Notes:**
     *
     * - Repeated keywords count as a single keyword.
     * - Thesaurus expansions count towards the `partialMatchKeywords` count.
     * - Stemming expansions **do not** count towards the `partialMatchKeywords` count.
     *
     * @notSupportedIn salesforcefree
     */
    partialMatchKeywords: ComponentOptions.buildNumberOption({ defaultValue: 5, min: 1, depend: 'enablePartialMatch' }),

    /**
     * An absolute or relative value indicating the minimum number of partial match expression keywords an item must contain to match the expression.
     *
     * See also the [`partialMatchKeywords`]{@link Querybox.options.partialMatchKeywords} option.
     *
     * **Notes:**
     * - A keyword and its stemming expansions count as a single keyword when evaluating whether an item meets the `partialMatchThreshold`.
     * - When a relative `partialMatchThreshold` does not yield a whole integer, the fractional part is truncated (e.g., `3.6` becomes `3`).
     *
     * @notSupportedIn salesforcefree
     */
    partialMatchThreshold: ComponentOptions.buildStringOption({ defaultValue: '50%', depend: 'enablePartialMatch' }),

    /**
     * Whether to trigger a query when clearing the `Querybox`.
     *
     * Default value is `false`.
     */
    triggerQueryOnClear: ComponentOptions.buildBooleanOption({ defaultValue: false })
  };
  MagicBoxImpl;

  public magicBox: MagicBoxInstance;
  private lastQuery: string;
  private searchAsYouTypeTimeout: number;

  /**
   * Creates a new `Querybox component`. Creates a new `Coveo.Magicbox` instance and wraps the Magicbox methods
   * (`onblur`, `onsubmit` etc.). Binds event on `buildingQuery` and before redirection (for standalone box).
   * @param element The HTMLElement on which to instantiate the component. This cannot be an HTMLInputElement for
   * technical reasons.
   * @param options The options for the `Querybox` component.
   * @param bindings The bindings that the component requires to function normally. If not set, these will be
   * automatically resolved (with a slower execution time).
   */
  constructor(public element: HTMLElement, public options?: IQueryboxOptions, public bindings?: IComponentBindings) {
    super(element, Querybox.ID, bindings);

    if (element instanceof HTMLInputElement) {
      this.logger.error('Querybox cannot be used on an HTMLInputElement');
    }

    this.options = ComponentOptions.initComponentOptions(element, Querybox, options);
    new QueryboxOptionsProcessing(this).postProcess();

    $$(this.element).toggleClass('coveo-query-syntax-disabled', this.options.enableQuerySyntax == false);
    this.magicBox = createMagicBox(
      element,
      new Grammar('Query', {
        Query: '[Term*][Spaces?]',
        Term: '[Spaces?][Word]',
        Spaces: / +/,
        Word: /[^ ]+/
      }),
      {
        inline: true
      }
    );

    const input = $$(this.magicBox.element).find('input');
    if (input) {
      $$(input).setAttribute('aria-label', this.options.placeholder || l('Search'));
    }

    this.bind.onRootElement(QueryEvents.buildingQuery, (args: IBuildingQueryEventArgs) => this.handleBuildingQuery(args));
    this.bind.onRootElement(StandaloneSearchInterfaceEvents.beforeRedirect, () => this.updateQueryState());
    this.bind.onQueryState(MODEL_EVENTS.CHANGE_ONE, QUERY_STATE_ATTRIBUTES.Q, (args: IAttributeChangedEventArg) =>
      this.handleQueryStateChanged(args)
    );

    if (this.options.enableSearchAsYouType) {
      $$(this.element).addClass('coveo-search-as-you-type');
      this.magicBox.onchange = () => {
        this.searchAsYouType();
      };
    }

    this.magicBox.onsubmit = () => {
      this.submit();
    };

    this.magicBox.onblur = () => {
      this.updateQueryState();
    };

    this.magicBox.onclear = () => {
      this.updateQueryState();
      if (this.options.triggerQueryOnClear) {
        this.usageAnalytics.logSearchEvent<IAnalyticsNoMeta>(analyticsActionCauseList.searchboxClear, {});
        this.triggerNewQuery(false);
      }
    };
  }

  /**
   * Adds the current content of the input to the query and triggers a query if the current content of the input has
   * changed since last submit.
   *
   * Also logs the `serachboxSubmit` event in the usage analytics.
   */
  public submit(): void {
    this.magicBox.clearSuggestion();
    this.updateQueryState();
    this.usageAnalytics.logSearchEvent<IAnalyticsNoMeta>(analyticsActionCauseList.searchboxSubmit, {});
    this.triggerNewQuery(false);
  }

  /**
   * Sets the content of the input.
   *
   * @param text The string to set in the input.
   */
  public setText(text: string): void {
    this.magicBox.setText(text);
    this.updateQueryState();
  }

  /**
   * Clears the content of the input.
   *
   * See also the [`triggerQueryOnClear`]{@link Querybox.options.triggerQueryOnClear} option.
   */
  public clear(): void {
    this.magicBox.clear();
  }

  /**
   * Gets the content of the input.
   *
   * @returns {string} The content of the input.
   */
  public getText(): string {
    return this.magicBox.getText();
  }

  /**
   * Gets the result from the input.
   *
   * @returns {Result} The result.
   */
  public getResult() {
    return this.magicBox.getResult();
  }

  /**
   * Gets the displayed result from the input.
   *
   * @returns {Result} The displayed result.
   */
  public getDisplayedResult(): Result {
    return this.magicBox.getDisplayedResult();
  }

  /**
   * Gets the current cursor position in the input.
   *
   * @returns {number} The cursor position (index starts at 0).
   */
  public getCursor(): number {
    return this.magicBox.getCursor();
  }

  /**
   * Gets the result at cursor position.
   *
   * @param match {string | { (result): boolean }} The match condition.
   *
   * @returns {Result[]} The result.
   */
  public resultAtCursor(match?: string | { (result): boolean }) {
    return this.magicBox.resultAtCursor(match);
  }

  private handleBuildingQuery(args: IBuildingQueryEventArgs): void {
    Assert.exists(args);
    Assert.exists(args.queryBuilder);

    this.updateQueryState();
    this.lastQuery = this.magicBox.getText();
    new QueryboxQueryParameters(this.options).addParameters(args.queryBuilder, this.lastQuery);
  }

  private triggerNewQuery(searchAsYouType: boolean): void {
    clearTimeout(this.searchAsYouTypeTimeout);
    let text = this.magicBox.getText();
    if (this.lastQuery != text && text != null) {
      this.lastQuery = text;
      this.queryController.executeQuery({
        searchAsYouType: searchAsYouType,
        logInActionsHistory: true
      });
    }
  }

  private updateQueryState(): void {
    this.queryStateModel.set(QueryStateModel.attributesEnum.q, this.magicBox.getText());
  }

  private handleQueryStateChanged(args: IAttributeChangedEventArg): void {
    Assert.exists(args);
    let q = <string>args.value;
    if (q != this.magicBox.getText()) {
      this.magicBox.setText(q);
    }
  }

  private searchAsYouType(): void {
    clearTimeout(this.searchAsYouTypeTimeout);
    this.searchAsYouTypeTimeout = window.setTimeout(() => {
      this.usageAnalytics.logSearchAsYouType<IAnalyticsNoMeta>(analyticsActionCauseList.searchboxAsYouType, {});
      this.triggerNewQuery(true);
    }, this.options.searchAsYouTypeDelay);
  }
}
Initialization.registerAutoCreateComponent(Querybox);
