import 'styling/_OmniboxResultList';
import * as _ from 'underscore';
import { IPopulateOmniboxEventArgs, OmniboxEvents } from '../../events/OmniboxEvents';
import { IBuildingQueryEventArgs, QueryEvents } from '../../events/QueryEvents';
import { InitializationEvents } from '../../EventsModules';
import { exportGlobally } from '../../GlobalExports';
import { Assert } from '../../misc/Assert';
import { Logger } from '../../misc/Logger';
import { IQueryResult } from '../../rest/QueryResult';
import { IQueryResults } from '../../rest/QueryResults';
import { l } from '../../strings/Strings';
import { $$ } from '../../utils/Dom';
import { Utils } from '../../utils/Utils';
import { analyticsActionCauseList, IAnalyticsNoMeta } from '../Analytics/AnalyticsActionListMeta';
import { logSearchBoxSubmitEvent } from '../Analytics/SharedAnalyticsCalls';
import { Component } from '../Base/Component';
import { IComponentBindings } from '../Base/ComponentBindings';
import { ComponentOptions } from '../Base/ComponentOptions';
import { IQueryExpression } from '../Base/IComponentOptions';
import { Initialization } from '../Base/Initialization';
import { ResultList } from '../ResultList/ResultList';
import { IResultListOptions } from '../ResultList/ResultListOptions';
import OmniboxModuleDefintion = require('../Omnibox/Omnibox');

export interface IOmniboxResultListOptions extends IResultListOptions {
  omniboxZIndex?: number;
  onSelect?: (result: IQueryResult, resultElement: HTMLElement, omniboxObject: IPopulateOmniboxEventArgs, event?: Event) => void;
  headerTitle?: string;
  queryOverride?: IQueryExpression;
}

/**
 * The OmniboxResultList component behaves exactly like the {@link ResultList} component (which it extends), except that
 * it renders itself inside the {@link Omnibox} component.
 *
 * This component can provide a kind of search-as-you-type functionality, allowing you to easily render complex Result
 * Templates inside the Omnibox component.
 *
 * **Example:**
 *
 * ```html
 * <div class="CoveoOmniboxResultList">
 *   <script class="result-template" type="text/x-underscore">
 *     <div>
 *       <a class='CoveoResultLink'></a>
 *     </div>
 *   </script>
 * </div>
 * ```
 */
export class OmniboxResultList extends ResultList implements IComponentBindings {
  static ID = 'OmniboxResultList';

  /**
   * Specifies a list a css class that should be ignored when the end user click result in the omnibox
   *
   * Any element that is specified here should normally be able to handle the standard click event.
   *
   * Any element that does not match this css class and that is clicked will trigger a redirection by the OmniboxResultList.
   */
  static elementsToIgnore = [
    'coveo-field-table-toggle-caption',
    'CoveoFollowItem',
    'CoveoPrintableUri',
    'CoveoQuickview',
    'CoveoResultLink',
    'CoveoResultRating',
    'CoveoResultTagging',
    'CoveoYouTubeThumbnail'
  ];

  static doExport = () => {
    exportGlobally({
      OmniboxResultList: OmniboxResultList
    });
  };

  /**
   * The options for the component
   * @componentOptions
   */
  static options: IOmniboxResultListOptions = {
    layout: ComponentOptions.buildStringOption({
      defaultValue: 'list',
      postProcessing: optionSetByUser => {
        if (optionSetByUser != 'list') {
          const logger = new Logger(OmniboxResultList);
          logger.warn(`Cannot apply layout ${optionSetByUser} on the OmniboxResultListComponent`);
          logger.warn(`OmniboxResultList does not support any layout other than "list"`);
        }
        return 'list';
      }
    }),
    /**
     * Specifies the z-index at which to render the ResultList inside the Omnibox.
     *
     * Default value is `51`. Minimum value is `16` ({@link Facet} components are at `50` by default)
     */
    omniboxZIndex: ComponentOptions.buildNumberOption({ defaultValue: 51, min: 16 }),

    /**
     * Specifies the title to use for this section.
     *
     * Default value is the localized string for `Suggested Results`.
     */
    headerTitle: ComponentOptions.buildStringOption(),

    /**
     * Specifies the override to use on the query sent to the OmniboxResultList component.
     *
     * Default value is `undefined`, which means no default override is specified.
     */
    queryOverride: ComponentOptions.buildQueryExpressionOption(),

    /**
     * Specifies the function to execute when the user selects a result suggestion.
     *
     * The default function opens the corresponding result URI in the browser.
     *
     * It is only possible to specify a value for this option in the {@link init} call of your search interface. You
     * cannot set it directly as an HTML attribute.
     *
     * **Example:**
     *
     * ```javascript
     * // You can call the init script using "pure" JavaScript:
     * Coveo.init(document.querySelector('#search'), {
     *    OmniboxResultList : {
     *        //Close the omnibox, change the selected HTMLElement background color and alert the result title.
     *        onSelect : function(result, resultElement, omniBoxObject) {
     *            omniBoxObject.close();
     *            resultElement.css('background-color', 'red');
     *            alert(result.title);
     *        }
     *     }
     * })
     *
     * // Or you can call the init script using the jQuery extension:
     * $("#search").coveo("init", {
     *    OmniboxResultList : {
     *        //Close the Omnibox, change the selected HTMLElement background color and alert the result title.
     *        onSelect : function(result, resultElement, omniBoxObject) {
     *            omniBoxObject.close();
     *            resultElement.css('background-color', 'red');
     *            alert(result.title);
     *        }
     *     }
     * })
     * ```
     */
    onSelect: ComponentOptions.buildCustomOption<
      (result: IQueryResult, resultElement: HTMLElement, omniboxObject: IPopulateOmniboxEventArgs) => void
    >(() => {
      return null;
    })
  };

  private lastOmniboxRequest: { omniboxObject: IPopulateOmniboxEventArgs; resolve: (...args: any[]) => void };

  /**
   * Creates a new OmniboxResultList component.
   * @param element The HTMLElement on which to instantiate the component.
   * @param options The options for the OmniboxResultList 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?: IOmniboxResultListOptions, public bindings?: IComponentBindings) {
    super(element, options, bindings, OmniboxResultList.ID);
    this.options = ComponentOptions.initComponentOptions(element, OmniboxResultList, options);
    this.setupOptions();
    this.bind.onRootElement(OmniboxEvents.populateOmnibox, (args: IPopulateOmniboxEventArgs) => this.handlePopulateOmnibox(args));
    this.bind.onRootElement(QueryEvents.buildingQuery, (args: IBuildingQueryEventArgs) => this.handleBuildingQuery(args));
    this.bind.onRootElement(InitializationEvents.afterComponentsInitialization, () => {
      this.handleAfterComponentInit();
    });
  }

  /**
   * Builds and returns an array of `HTMLElement` from the {@link IQueryResults} set received as an argument.
   * @param results The IQueryResults set to build an array of `HTMLElement` from.
   */
  public buildResults(results: IQueryResults): Promise<HTMLElement[]> {
    const builtResults: HTMLElement[] = [];
    const builtPromises = _.map(results.results, (result: IQueryResult) => {
      return this.buildResult(result).then((resultElement: HTMLElement) => {
        $$(resultElement).addClass('coveo-omnibox-selectable');
        resultElement['no-text-suggestion'] = true;

        $$(resultElement).on(['keyboardSelect', 'click'], (e: Event) => this.handleOmniboxElementSelection(e, resultElement, result));

        return this.autoCreateComponentsInsideResult(resultElement, result).initResult.then(() => {
          builtResults.push(resultElement);
          return resultElement;
        });
      });
    });

    return Promise.all(builtPromises).then(() => {
      return builtResults;
    });
  }

  /**
   * Creates a result container and appends each element from the received `HTMLElement` array to it. For each element
   * it appends to the result container, this method triggers a `newResultDisplayed` event. Once all elements have been
   * appended to the result container, the method triggers a `newResultsDisplayed` event.
   * @param resultElements The array of `HTMLElement` to render.
   * @param append
   */
  public renderResults(resultElements: HTMLElement[], append = false) {
    $$(this.options.resultsContainer).empty();

    if (!this.lastOmniboxRequest) {
      return Promise.resolve(null);
    }

    if (resultElements.length) {
      this.appendHeaderIfTitleIsSpecified();
      this.appendResults(resultElements);
    }

    this.resolveLastOmniboxRequest();

    return Promise.resolve(null);
  }

  private handleAfterComponentInit() {
    const omniboxElement: HTMLElement = $$(this.root).find(`.${Component.computeCssClassNameForType('Omnibox')}`);
    if (omniboxElement) {
      const omnibox = <OmniboxModuleDefintion.Omnibox>Component.get(omniboxElement);
      const magicBox = omnibox.magicBox;
      magicBox.onsubmit = () => {
        logSearchBoxSubmitEvent(this.usageAnalytics);
        this.queryController.executeQuery();
      };
    }
  }

  private appendHeaderIfTitleIsSpecified() {
    if (this.options.headerTitle) {
      this.options.resultsContainer.appendChild(
        $$(
          'div',
          { className: 'coveo-omnibox-result-list-header' },
          $$('span', { className: 'coveo-icon-omnibox-result-list' }).el,
          $$('span', { className: 'coveo-caption' }, l(this.options.headerTitle)).el
        ).el
      );
    }
  }

  private appendResults(resultElements: HTMLElement[]) {
    _.each(resultElements, (resultElement: HTMLElement) => {
      this.options.resultsContainer.appendChild(resultElement);
      this.triggerNewResultDisplayed(Component.getResult(resultElement), resultElement);
    });

    this.triggerNewResultsDisplayed();
  }

  private resolveLastOmniboxRequest() {
    if ($$(this.options.resultsContainer).findAll('.coveo-omnibox-selectable').length == 0) {
      this.lastOmniboxRequest.resolve({ element: null, zIndex: this.options.omniboxZIndex });
    } else {
      this.lastOmniboxRequest.resolve({ element: this.options.resultsContainer, zIndex: this.options.omniboxZIndex });
    }
  }

  protected handleChangeLayout() {
    // Since OmniboxResultList does not allow multiple layout,
    // we simply ignore the event change since the component does not have anything meaningful to do.
    return;
  }

  private setupOptions() {
    this.logger.info('Disabling infinite scroll for OmniboxResultList', this);
    this.options.enableInfiniteScroll = false;
    this.options.onSelect = this.options.onSelect || this.onRowSelection;
  }

  private handlePopulateOmnibox(args: IPopulateOmniboxEventArgs) {
    const promise = new Promise((resolve, reject) => {
      this.queryController.executeQuery({
        searchAsYouType: true,
        shouldRedirectStandaloneSearchbox: false,
        beforeExecuteQuery: () => this.usageAnalytics.logSearchAsYouType<IAnalyticsNoMeta>(analyticsActionCauseList.searchboxSubmit, {})
      });
      this.lastOmniboxRequest = { omniboxObject: args, resolve: resolve };
    });
    args.rows.push({
      deferred: promise
    });
  }

  protected handleBuildingQuery(args: IBuildingQueryEventArgs) {
    Assert.exists(args);
    if (Utils.isNonEmptyString(this.options.queryOverride)) {
      args.queryBuilder.constantExpression.add(this.options.queryOverride);
    }
  }

  private handleOmniboxElementSelection(e: Event, resultElement: HTMLElement, result: IQueryResult) {
    if (e && e.target && this.otherComponentShouldHandleSelection(e, resultElement)) {
      return;
    }

    if (this.lastOmniboxRequest) {
      this.options.onSelect.call(this, result, resultElement, this.lastOmniboxRequest.omniboxObject, e);
    }
  }

  private otherComponentShouldHandleSelection(e: Event, resultElement: HTMLElement) {
    // Other components can "trap" the click event, and instead trigger the "standard" component behaviour.
    // So, for example, if someones clicks the ResultLink directly, we want the result link code to execute to redirect to the result, and not the OmniboxResultList selection code.
    // Same for Quickview, YouTubeThumbnail, etc.
    let current = e.target as HTMLElement;
    let otherComponentWillHandleClick = false;

    while (current && current != resultElement) {
      otherComponentWillHandleClick =
        _.find(OmniboxResultList.elementsToIgnore, elementToIgnore => $$(current).hasClass(elementToIgnore)) != null;

      if (otherComponentWillHandleClick) {
        break;
      }

      current = current.parentElement;
    }
    return otherComponentWillHandleClick;
  }

  private onRowSelection(result: IQueryResult, resultElement: HTMLElement, omniboxObject: IPopulateOmniboxEventArgs, e: Event) {
    this.usageAnalytics.logClickEvent(
      analyticsActionCauseList.documentOpen,
      { author: Utils.getFieldValue(result, 'author') },
      result,
      this.root
    );
    window.location.href = result.clickUri;
  }

  protected initResultContainerAddToDom() {
    //This function is overwritten and don't do anything because we don't want to append the result container
    //to the DOM. If the resultContainer was to be appended to the DOM, this could lead to result appearing
    //outside of the magicBox so the rest of the page would be push down
  }
}

Initialization.registerAutoCreateComponent(OmniboxResultList);
