import 'styling/_Tab';
import { each, indexOf, map } from 'underscore';
import { InitializationEvents } from '../../events/InitializationEvents';
import { IBuildingQueryEventArgs, QueryEvents } from '../../events/QueryEvents';
import { exportGlobally } from '../../GlobalExports';
import { Assert } from '../../misc/Assert';
import { IAttributeChangedEventArg, MODEL_EVENTS } from '../../models/Model';
import { QueryStateModel, QUERY_STATE_ATTRIBUTES } from '../../models/QueryStateModel';
import { SearchEndpoint } from '../../rest/SearchEndpoint';
import { AccessibleButton } from '../../utils/AccessibleButton';
import { $$ } from '../../utils/Dom';
import { Utils } from '../../utils/Utils';
import { analyticsActionCauseList, IAnalyticsInterfaceChange } from '../Analytics/AnalyticsActionListMeta';
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 { ResponsiveTabs } from '../ResponsiveComponents/ResponsiveTabs';
import { ValidLayout } from '../ResultLayoutSelector/ValidLayout';

export interface ITabOptions {
  expression?: IQueryExpression;
  constant?: boolean;
  id?: string;
  icon?: string;
  caption?: string;
  sort?: string;
  layout?: string;
  endpoint?: SearchEndpoint;
  enableDuplicateFiltering?: boolean;
  pipeline?: string;
  maximumAge?: number;
  enableResponsiveMode?: boolean;
  dropdownHeaderLabel?: string;
}

/**
 * The Tab component renders a widget that allows the end user to select a specific search interface.
 *
 * This component attaches itself to a `div` element. It is in charge of adding an advanced expression to the outgoing
 * query in order to refine the results.
 *
 * The Tab component can also hide and show different parts of the UI. For each individual component in the UI, you can
 * specify whether you wish to include or exclude that component when the user selects a certain Tab (see [Using Components
 * Only on Specific Tabs](https://docs.coveo.com/en/508/javascript-search-framework/using-components-only-on-specific-tabs)).
 *
 * **Setting a New Endpoint for a Tab:**
 *
 * A Tab can use a custom endpoint when performing a query. Of course, you need to make sure that the endpoint exists in
 * the array of Coveo.SearchEndpoint.endpoints (see {@link SearchEndpoint.endpoints}).
 *
 * ```
 * Coveo.SearchEndpoint.endpoints["specialEndpoint"] = new Coveo.SearchEndpoint({
 *     restUri : 'https://somewhere.com/rest/search'
 * })
 *
 * [ ... ]
 *
 * <div class='CoveoTab' data-endpoint='specialEndpoint'></div>
 *
 * ```
 */
export class Tab extends Component {
  static ID = 'Tab';

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

  /**
   * The options for a Tab
   * @componentOptions
   */
  static options: ITabOptions = {
    /**
     * Specifies a unique ID for the Tab.
     *
     * Specifying a value for this option is necessary for this component to work.
     */
    id: ComponentOptions.buildStringOption({ required: true, section: 'Common Options' }),

    /**
     * Specifies the caption of the Tab.
     *
     * Specifying a value for this option is necessary for this component to work.
     */
    caption: ComponentOptions.buildLocalizedStringOption({ required: true, section: 'Common Options' }),

    /**
     * Specifies an icon to use for the Tab.
     *
     * @deprecated This options is mostly kept for legacy reasons. If possible, you should avoid using it.
     */
    icon: ComponentOptions.buildStringOption(),

    /**
     * Specifies an advanced expression or filter that the Tab should add to any outgoing query.
     *
     * **Example:**
     *
     * `@objecttype==Message`
     *
     * Default value is `undefined` and the Tab applies no additional expression or filter to the query.
     */
    expression: ComponentOptions.buildQueryExpressionOption({ section: 'Common Options' }),

    /**
     * Specifies the {@link SearchEndpoint} to point to when performing queries from within the Tab.
     *
     * By default, the Tab uses the "default" endpoint.
     */
    endpoint: ComponentOptions.buildCustomOption(endpoint => (endpoint != null ? SearchEndpoint.endpoints[endpoint] : null)),

    /**
     * Specifies the default sort criteria to use when selecting the Tab. A {@link Sort} component with the same
     * parameter needs to be present in the search interface in order for this option to function properly.
     *
     * **Examples:**
     *
     * - `data-sort='relevancy'`
     * - `data-sort='date descending'`
     *
     * Default value is `undefined` and the normal {@link Sort} component behavior applies.
     */
    sort: ComponentOptions.buildStringOption(),

    /**
     * Specifies the default layout to display when the user selects the Tab (see {@link ResultList.options.layout} and
     * {@link ResultLayout}).
     *
     * See the {@link ValidLayout} type for the list of possible values.
     *
     * If not specified, it will default to 'list'.
     *
     * See also [Result Layouts](https://docs.coveo.com/en/360/).
     *
     * Default value is `undefined` and the component selects the first available layout.
     */
    layout: ComponentOptions.buildStringOption<ValidLayout>(),

    /**
     * Specifies whether to include the {@link Tab.options.expression} in the constant part of the query.
     *
     * The index specially optimizes the constant part of the query to execute faster. However, you must be careful not
     * to include dynamic query expressions, otherwise the cache will lose its efficiency.
     *
     * Default value is `true`.
     */
    constant: ComponentOptions.buildBooleanOption({ defaultValue: true, section: 'Filtering' }),

    /**
     * Whether to filter out duplicates, so that items resembling one another only appear once in the query results.
     *
     * **Notes:**
     * - Two items must be at least 85% similar to one another to be considered duplicates.
     * - When a pair of duplicates is found, only the higher-ranked item of the two is kept in the result set.
     * - Enabling this feature can make the total result count less precise, as only the requested page of query results is submitted to duplicate filtering.
     * - The default value for this option can be modified through the {@link SearchInterface} component.
     */
    enableDuplicateFiltering: ComponentOptions.buildBooleanOption({ defaultValue: false }),

    /**
     * Specifies the name of the query pipeline to use for the queries when the Tab is selected.
     *
     * You can specify a value for this option if your index is in a Coveo Cloud organization in which pipelines have
     * been created (see [Adding and Managing Query Pipelines](https://docs.coveo.com/en/1791/)).
     *
     * Default value is `undefined`, which means that pipeline selection conditions defined in the Coveo Cloud
     * organization apply.
     */
    pipeline: ComponentOptions.buildStringOption(),

    /**
     * Specifies the maximum age (in milliseconds) that cached query results can have to still be usable as results
     * instead of performing a new query on the index from within the Tab. The cache is located in the Coveo Search API
     * (which resides between the index and the search interface).
     *
     * If cached results that are older than the age you specify in this option are available, a new query will be
     * performed on the index anyhow.
     *
     * On high-volume public web sites, specifying a higher value for this option can greatly improve query response
     * time at the cost of result freshness.
     *
     * **Note:**
     *
     * > It is also possible to set a maximum cache age for the entire {@link SearchInterface} rather than for a single
     * > Tab (see {@link SearchInterface.options.maximumAge}).
     *
     * Default value is `undefined` and the Coveo Search API determines the maximum cache age. This is typically
     * equivalent to 30 minutes (see [maximumAge](https://docs.coveo.com/en/13#operation/searchUsingPost-maximumAge)).
     */
    maximumAge: ComponentOptions.buildNumberOption(),

    /**
     * Specifies whether to enable responsive mode for tabs. Responsive mode makes overflowing tabs disappear, instead
     * making them available using a dropdown button. Responsive tabs are enabled either when tabs overflow or when the
     * width of the search interface becomes too small.
     *
     * Disabling responsive mode for one Tab also disables it for all tabs. Therefore, you only need to set this option
     * to `false` on one Tab to disable responsive mode.
     *
     * Default value is `true`.
     *
     * @availablesince [October 2016 Release (v1.1550.5)](https://docs.coveo.com/en/309/#october-2016-release-v115505)
     */
    enableResponsiveMode: ComponentOptions.buildBooleanOption({ defaultValue: true, section: 'ResponsiveOptions' }),

    /**
     * Specifies the label of the button that allows to show the hidden tabs when in responsive mode.
     *
     * If more than one Tab in the search interface specifies a value for this option, then the framework uses the first
     * occurrence of the option.
     *
     * The default value is `"More"`.
     */
    dropdownHeaderLabel: ComponentOptions.buildLocalizedStringOption({ section: 'ResponsiveOptions' })
  };

  /**
   * Creates a new Tab. Binds on buildingQuery event as well as an event on click of the element.
   * @param element The HTMLElement on which to instantiate the component. Normally a `div`.
   * @param options The options for the Tab 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?: ITabOptions, bindings?: IComponentBindings) {
    super(element, Tab.ID, bindings);

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

    this.bind.onRootElement(QueryEvents.buildingQuery, (args: IBuildingQueryEventArgs) => this.handleBuildingQuery(args));
    this.bind.onRootElement(InitializationEvents.afterInitialization, () => this.handleAfterInitialization());
    this.bind.onQueryState(MODEL_EVENTS.CHANGE_ONE, QUERY_STATE_ATTRIBUTES.T, (args: IAttributeChangedEventArg) =>
      this.handleQueryStateChanged(args)
    );

    new AccessibleButton()
      .withElement(element)
      .withSelectAction(() => this.select())
      .withTitle(this.options.caption)
      .withOwner(this.bind)
      .build();

    this.render();
    ResponsiveTabs.init(this.root, this, this.options);
  }

  /**
   * Selects the current Tab.
   *
   * Also logs the `interfaceChange` event in the usage analytics with the new current {@link Tab.options.id} as metada
   * and triggers a new query.
   */
  public select() {
    if (!this.disabled) {
      const state = {
        t: this.options.id,
        sort: this.options.sort || QueryStateModel.defaultAttributes.sort
      } as any;

      if (this.options.layout) {
        state.layout = this.options.layout;
      }

      this.queryStateModel.setMultiple(state);

      this.usageAnalytics.logSearchEvent<IAnalyticsInterfaceChange>(analyticsActionCauseList.interfaceChange, {
        interfaceChangeTo: this.options.id
      });
      this.queryController.executeQuery();
    }
  }

  /**
   * Indicates whether the HTMLElement argument is included in the Tab. *Included* elements are shown when the Tab is
   * selected, whereas *excluded* elements are not.
   * @param element The HTMLElement to verify.
   * @returns {boolean} `true` if the HTMLElement is included in the Tab; `false` if it is excluded.
   */
  public isElementIncludedInTab(element: HTMLElement): boolean {
    Assert.exists(element);

    const includedTabs = this.splitListOfTabs(element.getAttribute('data-tab'));
    const excludedTabs = this.splitListOfTabs(element.getAttribute('data-tab-not'));
    Assert.check(
      !(includedTabs.length != 0 && excludedTabs.length != 0),
      'You cannot both explicitly include and exclude an element from tabs.'
    );

    return (
      (includedTabs.length != 0 && indexOf(includedTabs, this.options.id) != -1) ||
      (excludedTabs.length != 0 && indexOf(excludedTabs, this.options.id) == -1) ||
      (includedTabs.length == 0 && excludedTabs.length == 0)
    );
  }

  private render() {
    const icon = this.options.icon;
    if (Utils.isNonEmptyString(icon)) {
      const iconSpan = $$('span').el;
      $$(iconSpan).addClass(['coveo-icon', icon]);
      this.element.insertBefore(iconSpan, this.element.firstChild);
    }

    const caption = this.options.caption;
    if (Utils.isNonEmptyString(caption)) {
      const captionP = document.createElement('p');
      $$(captionP).text(caption);
      this.element.appendChild(captionP);
    }
  }

  protected handleBuildingQuery(data: IBuildingQueryEventArgs) {
    Assert.exists(data);
    if (!this.disabled && this.isSelected()) {
      data.queryBuilder.tab = this.options.id;

      if (Utils.isNonEmptyString(this.options.expression)) {
        if (this.options.constant) {
          data.queryBuilder.constantExpression.add(this.options.expression);
        } else {
          data.queryBuilder.advancedExpression.add(this.options.expression);
        }
      }

      if (this.options.enableDuplicateFiltering) {
        data.queryBuilder.enableDuplicateFiltering = true;
      }

      if (this.options.pipeline != null) {
        data.queryBuilder.pipeline = this.options.pipeline;
      }

      if (this.options.maximumAge != null) {
        data.queryBuilder.maximumAge = this.options.maximumAge;
      }
    }
  }

  private handleQueryStateChanged(data: IAttributeChangedEventArg) {
    Assert.exists(data);
    if (!this.disabled && this.isSelected()) {
      $$(this.element).addClass('coveo-selected');
      this.queryController.setEndpoint(this.options.endpoint);
      this.showAndHideAppropriateElements();
    } else {
      $$(this.element).removeClass('coveo-selected');
    }
  }

  private handleAfterInitialization() {
    if (this.isSelected() && this.options.layout) {
      this.queryStateModel.set(QUERY_STATE_ATTRIBUTES.LAYOUT, this.options.layout);
    }
  }

  protected isSelected(): boolean {
    const activeTab = this.queryStateModel.get(QueryStateModel.attributesEnum.t);
    return activeTab == this.options.id;
  }

  private showAndHideAppropriateElements() {
    const showElements = [];
    const hideElements = [];

    each($$(this.root).findAll('[data-tab],[data-tab-not]'), element => {
      if (this.isElementIncludedInTab(element)) {
        this.toggleAllComponentsUnder(element, true);
        showElements.push(element);
      } else {
        this.toggleAllComponentsUnder(element, false);
        hideElements.push(element);
      }
    });

    $$(this.root).one(QueryEvents.querySuccess, () => {
      each(showElements, elem => $$(elem).removeClass('coveo-tab-disabled'));
      each(hideElements, elem => $$(elem).addClass('coveo-tab-disabled'));
    });
  }

  private splitListOfTabs(value: string): string[] {
    if (Utils.exists(value)) {
      return map(value.split(','), tab => Utils.trim(tab));
    } else {
      return [];
    }
  }

  private toggleAllComponentsUnder(element: HTMLElement, enable: boolean) {
    Assert.exists(element);

    const togglePossibleComponent = (possibleComponent: HTMLElement) => {
      const possibleCmp = Component.get(possibleComponent, undefined, true);
      if (possibleCmp) {
        if (enable) {
          possibleCmp.enable();
        } else {
          possibleCmp.disable();
        }
      }
    };

    togglePossibleComponent(element);
    each($$(element).findAll('*'), el => {
      togglePossibleComponent(el);
    });
  }

  public enable() {
    super.enable();
    this.element.style.display = '';
  }

  public disable() {
    super.disable();
    this.element.style.display = 'none';
  }
}

Initialization.registerAutoCreateComponent(Tab);
