import * as _ from 'underscore';
import { IPopulateOmniboxEventArgs, OmniboxEvents } from '../../events/OmniboxEvents';
import { QueryEvents } from '../../events/QueryEvents';
import { exportGlobally } from '../../GlobalExports';
import { Assert } from '../../misc/Assert';
import { QueryStateModel } from '../../models/QueryStateModel';
import { IStringMap } from '../../rest/GenericParam';
import { l } from '../../strings/Strings';
import { AccessibleButton } from '../../utils/AccessibleButton';
import { $$ } from '../../utils/Dom';
import { analyticsActionCauseList, IAnalyticsTopSuggestionMeta } from '../Analytics/AnalyticsActionListMeta';
import { Component } from '../Base/Component';
import { IComponentBindings } from '../Base/ComponentBindings';
import { ComponentOptions } from '../Base/ComponentOptions';
import { Initialization } from '../Base/Initialization';
import { ISuggestionForOmniboxOptions, ISuggestionForOmniboxTemplate, SuggestionForOmnibox } from '../Misc/SuggestionForOmnibox';
import { StandaloneSearchInterface } from '../SearchInterface/SearchInterface';

export interface IAnalyticsSuggestionsOptions extends ISuggestionForOmniboxOptions {}

/**
 * The AnalyticsSuggestion component provides query suggestions based on the queries that a Coveo Analytics service most
 * commonly logs (see [`topQueries`](https://platform.cloud.coveo.com/docs?urls.primaryName=Usage%20Analytics%20Read#/Statistics%20API%20-%20Version%2015/get__v15_stats_topQueries)).
 *
 * This component orders possible query suggestions by their respective number of successful item views, thus
 * prioritizing the most relevant query suggestions. Consequently, when better options are available, this component
 * does not suggest queries resulting in no clicks from users or requiring refinements.
 *
 * The query suggestions appear in the {@link Omnibox} Component. The AnalyticsSuggestion component strongly
 * relates to the {@link Analytics} component. While a user is typing in a query box, the AnalyticsSuggestion component
 * allows them to see and select the most commonly used and relevant queries.
 *
 * @deprecated This component is exposed for legacy reasons. If possible, you should avoid using this component.
 * Instead, you should use the [`Omnibox`]{@link Omnibox}
 * [`enableQuerySuggestAddon`]{@link Omnibox.options.enableQuerySuggestAddon} option.
 */
export class AnalyticsSuggestions extends Component {
  static ID = 'AnalyticsSuggestions';

  static doExport() {
    exportGlobally({
      AnalyticsSuggestions: AnalyticsSuggestions
    });
  }

  /**
   * The options for the component
   * @componentOptions
   */
  static options: IAnalyticsSuggestionsOptions = {
    /**
     * Specifies the z-index position at which the query suggestions render themselves in the {@link Omnibox}
     * component. Higher values are placed first.
     *
     * Default value is `52` and minimum value is `0`.
     */
    omniboxZIndex: ComponentOptions.buildNumberOption({ defaultValue: 52, min: 0 }),

    /**
     * Specifies the title of the query suggestions group in the {@link Omnibox} component. This option is not available
     * when using the default Lightning Friendly Theme.
     *
     * Default value is the localized string for `"Suggested Queries"`.
     */
    headerTitle: ComponentOptions.buildLocalizedStringOption({
      localizedString: () => l('SuggestedQueries')
    }),

    /**
     * Specifies the number of query suggestions to request and display in the {@link Omnibox} component.
     *
     * Default value is `5` and minimum value is `1`.
     */
    numberOfSuggestions: ComponentOptions.buildNumberOption({ defaultValue: 5, min: 1 })
  };

  private suggestionForOmnibox: SuggestionForOmnibox;
  private partialQueries: string[] = [];
  private lastSuggestions: string[] = [];
  private resultsToBuildWith = [];
  private currentlyDisplayedSuggestions: { [suggestion: string]: { element: HTMLElement; pos: number } };

  /**
   * Creates a new AnalyticsSuggestions component.
   *
   * Also binds event handlers so that when a user selects a suggestion, an `omniboxFromLink` usage analytics event is
   * logged if the suggestion comes from a standalone search box, or an `omniboxAnalytics` usage analytics
   * event is logged otherwise.
   *
   * @param element The HTMLElement on which to instantiate the component.
   * @param options The options for the AnalyticsSuggestions 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(element: HTMLElement, public options: IAnalyticsSuggestionsOptions, bindings?: IComponentBindings) {
    super(element, AnalyticsSuggestions.ID, bindings);

    if (this.options && 'omniboxSuggestionOptions' in this.options) {
      this.options = _.extend(this.options, this.options['omniboxSuggestionOptions']);
    }

    this.options = ComponentOptions.initComponentOptions(element, AnalyticsSuggestions, this.options);

    const rowTemplate = (toRender: IStringMap<any>) => {
      const rowElement = $$('div', {
        className: 'magic-box-suggestion coveo-omnibox-selectable coveo-top-analytics-suggestion-row'
      });

      new AccessibleButton()
        .withElement(rowElement)
        .withLabel(toRender.rawValue)
        .build();

      if (toRender['data']) {
        rowElement.el.innerHTML = toRender['data'];
      }
      return rowElement.el.outerHTML;
    };

    this.options.onSelect = this.options.onSelect || this.onRowSelection;

    const suggestionStructure: ISuggestionForOmniboxTemplate = {
      row: rowTemplate
    };

    this.suggestionForOmnibox = new SuggestionForOmnibox(
      suggestionStructure,
      (value: string, args: IPopulateOmniboxEventArgs) => {
        this.options.onSelect.call(this, value, args);
      },
      (value: string, args: IPopulateOmniboxEventArgs) => {
        this.onRowTab.call(this, value, args);
      }
    );
    this.bind.onRootElement(OmniboxEvents.populateOmnibox, (args: IPopulateOmniboxEventArgs) => this.handlePopulateOmnibox(args));
    this.bind.onRootElement(QueryEvents.querySuccess, () => (this.partialQueries = []));
  }

  /**
   * Selects a currently displayed query suggestion. This implies that at least one suggestion must have been returned
   * at least once. The suggestion parameter can either be a number (0-based index position of the query suggestion to
   * select) or a string that matches the suggestion.
   *
   * @param suggestion
   */
  public selectSuggestion(suggestion: number);
  public selectSuggestion(suggestion: string);
  public selectSuggestion(suggestion: any) {
    if (this.currentlyDisplayedSuggestions) {
      if (isNaN(suggestion)) {
        if (this.currentlyDisplayedSuggestions[suggestion]) {
          $$(this.currentlyDisplayedSuggestions[suggestion].element).trigger('click');
        }
      } else {
        const currentlySuggested = <{ element: HTMLElement; pos: number }>_.findWhere(<any>this.currentlyDisplayedSuggestions, {
          pos: suggestion
        });
        if (currentlySuggested) {
          $$(currentlySuggested.element).trigger('click');
        }
      }
    }
  }

  private handlePopulateOmnibox(args: IPopulateOmniboxEventArgs) {
    Assert.exists(args);

    var promise = new Promise((resolve, reject) => {
      const searchPromise = this.usageAnalytics.getTopQueries({
        pageSize: this.options.numberOfSuggestions,
        queryText: args.completeQueryExpression.word
      });

      searchPromise.then((results: string[]) => {
        this.resultsToBuildWith = _.map(results, result => {
          return {
            value: result
          };
        });
        this.lastSuggestions = results;
        if (!_.isEmpty(this.resultsToBuildWith) && args.completeQueryExpression.word != '') {
          this.partialQueries.push(args.completeQueryExpression.word);
        }
        const element = this.suggestionForOmnibox.buildOmniboxElement(this.resultsToBuildWith, args);
        this.currentlyDisplayedSuggestions = {};
        if (element) {
          _.map($$(element).findAll('.coveo-omnibox-selectable'), (selectable, i?) => {
            this.currentlyDisplayedSuggestions[$$(selectable).text()] = {
              element: selectable,
              pos: i
            };
          });
        }
        resolve({
          element: element,
          zIndex: this.options.omniboxZIndex
        });
      });
      searchPromise.catch(() => {
        resolve({
          element: undefined
        });
      });
    });

    args.rows.push({ deferred: promise });
  }

  private onRowSelection(value: string, args: IPopulateOmniboxEventArgs) {
    args.clear();
    args.closeOmnibox();
    this.queryStateModel.set(QueryStateModel.attributesEnum.q, value);
    this.usageAnalytics.logSearchEvent<IAnalyticsTopSuggestionMeta>(this.getOmniboxAnalyticsEventCause(), {
      partialQueries: this.cleanCustomData(this.partialQueries),
      suggestionRanking: _.indexOf(_.pluck(this.resultsToBuildWith, 'value'), value),
      suggestions: this.cleanCustomData(this.lastSuggestions),
      partialQuery: args.completeQueryExpression.word
    });
    this.queryController.executeQuery();
  }

  private onRowTab(value: string, args: IPopulateOmniboxEventArgs) {
    args.clear();
    args.closeOmnibox();
    this.queryStateModel.set(QueryStateModel.attributesEnum.q, `${value}`);
    this.usageAnalytics.logCustomEvent<IAnalyticsTopSuggestionMeta>(
      this.getOmniboxAnalyticsEventCause(),
      {
        partialQueries: this.cleanCustomData(this.partialQueries),
        suggestionRanking: _.indexOf(_.pluck(this.resultsToBuildWith, 'value'), value),
        suggestions: this.cleanCustomData(this.lastSuggestions),
        partialQuery: args.completeQueryExpression.word
      },
      this.element
    );
  }

  private cleanCustomData(toClean: string[], rejectLength = 256) {
    // Filter out only consecutive values that are the identical
    toClean = _.compact(
      _.filter(toClean, (partial: string, pos?: number, array?: string[]) => {
        return pos === 0 || partial !== array[pos - 1];
      })
    );

    // Custom dimensions cannot be an array in analytics service: Send a string joined by ; instead.
    // Need to replace ;
    toClean = _.map(toClean, partial => {
      return partial.replace(/;/g, '');
    });

    // Reduce right to get the last X words that adds to less then rejectLength
    const reducedToRejectLengthOrLess = [];
    _.reduceRight(
      toClean,
      (memo: number, partial: string) => {
        const totalSoFar = memo + partial.length;
        if (totalSoFar <= rejectLength) {
          reducedToRejectLengthOrLess.push(partial);
        }
        return totalSoFar;
      },
      0
    );
    toClean = reducedToRejectLengthOrLess.reverse();
    const ret = toClean.join(';');

    // analytics service can store max 256 char in a custom event
    // if we're over that, call cleanup again with an arbitrary 10 less char accepted
    if (ret.length >= 256) {
      return this.cleanCustomData(toClean, rejectLength - 10);
    }

    return toClean.join(';');
  }

  private getOmniboxAnalyticsEventCause() {
    if (this.searchInterface instanceof StandaloneSearchInterface) {
      return analyticsActionCauseList.omniboxFromLink;
    }
    return analyticsActionCauseList.omniboxAnalytics;
  }
}
Initialization.registerAutoCreateComponent(AnalyticsSuggestions);
