import 'styling/DynamicFacet/_DynamicFacet';
import { IDynamicFacet, IDynamicFacetOptions } from './IDynamicFacet';
import { defer, difference, findIndex } from 'underscore';
import { $$ } from '../../utils/Dom';
import { exportGlobally } from '../../GlobalExports';
import { Component } from '../Base/Component';
import { IComponentBindings } from '../Base/ComponentBindings';
import { ComponentOptions } from '../Base/ComponentOptions';
import { Initialization } from '../Base/Initialization';
import { ResponsiveFacetOptions } from '../ResponsiveComponents/ResponsiveFacetOptions';
import { ResponsiveDynamicFacets } from '../ResponsiveComponents/ResponsiveDynamicFacets';
import { DynamicFacetBreadcrumbs } from './DynamicFacetBreadcrumbs';
import { DynamicFacetHeader } from './DynamicFacetHeader/DynamicFacetHeader';
import { DynamicFacetValues } from './DynamicFacetValues/DynamicFacetValues';
import { QueryEvents, IQuerySuccessEventArgs, IDoneBuildingQueryEventArgs } from '../../events/QueryEvents';
import { QueryStateModel } from '../../models/QueryStateModel';
import { DynamicFacetQueryController } from '../../controllers/DynamicFacetQueryController';
import { Utils } from '../../utils/Utils';
import { MODEL_EVENTS, IAttributesChangedEventArg } from '../../models/Model';
import { Assert } from '../../misc/Assert';
import { IFacetResponse } from '../../rest/Facet/FacetResponse';
import { IStringMap } from '../../rest/GenericParam';
import { isFacetSortCriteria, FacetSortCriteria } from '../../rest/Facet/FacetSortCriteria';
import { l } from '../../strings/Strings';
import { DeviceUtils } from '../../utils/DeviceUtils';
import { BreadcrumbEvents, IPopulateBreadcrumbEventArgs } from '../../events/BreadcrumbEvents';
import { IAnalyticsActionCause, analyticsActionCauseList, IAnalyticsFacetMeta } from '../Analytics/AnalyticsActionListMeta';
import { IAnalyticsFacetState } from '../Analytics/IAnalyticsFacetState';
import { IQueryOptions } from '../../controllers/QueryController';
import { DynamicFacetManager } from '../DynamicFacetManager/DynamicFacetManager';
import { QueryBuilder } from '../Base/QueryBuilder';
import { DynamicFacetSearch } from '../DynamicFacetSearch/DynamicFacetSearch';
import { ResultListUtils } from '../../utils/ResultListUtils';
import { IQueryResults } from '../../rest/QueryResults';
import { FacetType } from '../../rest/Facet/FacetRequest';
import { DependsOnManager, IDependentFacet, IDependentFacetCondition } from '../../utils/DependsOnManager';
import { DynamicFacetValueCreator } from './DynamicFacetValues/DynamicFacetValueCreator';
import { Logger } from '../../misc/Logger';
import { FacetUtils } from '../Facet/FacetUtils';

/**
 * The `DynamicFacet` component displays a *facet* of the results for the current query. A facet is a list of values for a
 * certain field occurring in the results, ordered using a configurable criteria (e.g., number of occurrences).
 *
 * The list of values is obtained using an array of [`FacetRequest`]{@link IFacetRequest} operations performed at the same time
 * as the main query.
 *
 * The `DynamicFacet` component allows the end-user to drill down inside a result set by restricting the result to certain
 * field values.
 *
 * This facet is more easy to use than the original [`Facet`]{@link Facet} component. It implements additional Coveo Machine Learning (Coveo ML) features
 * such as dynamic navigation experience (DNE).
 *
 * @notSupportedIn salesforcefree
 * @availablesince [May 2019 Release (v2.6063)](https://docs.coveo.com/en/2909/)
 */
export class DynamicFacet extends Component implements IDynamicFacet {
  static ID = 'DynamicFacet';
  static doExport = () => exportGlobally({ DynamicFacet });

  /**
   * The options for the DynamicFacet
   * @componentOptions
   */
  static options: IDynamicFacetOptions = {
    ...ResponsiveFacetOptions,

    /**
     * The unique identifier for this facet.
     *
     * Among other things, this is used to record and read the facet
     * state in the URL fragment identifier (see the
     * [`enableHistory`]{@link SearchInterface.options.enableHistory} `SearchInterface`
     * option).
     *
     * **Tip:** When several facets in a given search interface are based on
     * the same field, ensure that each of those facets has a distinct `id`.
     *
     * If specified, must contain between 1 and 60 characters.
     * Only alphanumeric (A-Za-z0-9), underscore (_), and hyphen (-) characters are kept; other characters are automatically removed.
     *
     * Defaults to the [`field`]{@link DynamicFacet.options.field} option value.
     *
     * @examples author-facet
     */
    id: ComponentOptions.buildStringOption({
      postProcessing: (value = '', options: IDynamicFacetOptions) => {
        const maxCharLength = 60;
        const sanitizedValue = value.replace(/[^A-Za-z0-9-_@]+/g, '');
        if (Utils.isNonEmptyString(sanitizedValue)) {
          return sanitizedValue.slice(0, maxCharLength - 1);
        }

        return options.field.slice(0, maxCharLength - 1);
      },
      section: 'CommonOptions'
    }),

    /**
     * The title to display for this facet.
     *
     * Defaults to the localized string for `NoTitle`.
     *
     * @examples Author
     */
    title: ComponentOptions.buildLocalizedStringOption({
      localizedString: () => l('NoTitle'),
      section: 'CommonOptions',
      priority: 10
    }),

    /**
     * The name of the field on which to base this facet.
     *
     * The field must of the `string` type.
     *
     * Must be prefixed by `@`, and must reference an existing field whose
     * **Facet** option is enabled.
     *
     * @externaldocs [Add or Edit Fields](https://docs.coveo.com/en/1982/)
     * @examples @author
     */
    field: ComponentOptions.buildFieldOption({ required: true, section: 'CommonOptions' }),

    /**
     * The sort criterion to use for this facet.
     *
     * See [`FacetSortCriteria`]{@link FacetSortCriteria} for the list and
     * description of allowed values.
     *
     * By default, the following behavior applies:
     *
     * - If the requested [`numberOfValues`]{@link DynamicFacet.options.numberOfValues}
     * is greater than or equal to the currently displayed number of values,
     * the [`alphanumeric`]{@link FacetSortCriteria.alphanumeric} criterion is
     * used.
     * - If the requested `numberOfValues` is less than the currently displayed
     * number of values and the facet is not currently expanded, the [`score`]{@link FacetSortCriteria.score}
     * criterion is used.
     * - Otherwise, the `alphanumeric` criterion is used.
     *
     * @examples score
     */
    sortCriteria: <FacetSortCriteria>ComponentOptions.buildStringOption({
      postProcessing: value => {
        if (!value) {
          return undefined;
        }

        if (isFacetSortCriteria(value)) {
          return value;
        }

        new Logger(value).warn('sortCriteria is not of the the allowed values: "score", "alphanumeric", "occurrences"');
        return undefined;
      },
      section: 'Sorting'
    }),

    /**
     * Specifies a custom order by which to sort the dynamic facet values.
     *
     * Custom-ordered values won't necessarily retrieve values from the index.
     *
     * **Example:**
     *
     * You could use this option to specify a logical order for support tickets, such as:
     * ```html
     * <div class="CoveoDynamicFacet" data-field="@ticketstatus" data-title="Ticket Status" data-tab="All" data-custom-sort="New,Opened,Feedback,Resolved"></div>
     * ```
     *
     * **Note:**
     * > The [`DynamicFacetRange`]{@link DynamicFacetRange} component does not support this option.
     */
    customSort: ComponentOptions.buildListOption<string>({ section: 'Sorting' }),

    /**
     * The number of values to request for this facet.
     *
     * Also determines the default maximum number of additional values to request each time this facet is expanded,
     * and the maximum number of values to display when this facet is collapsed (see the [`enableCollapse`]{@link DynamicFacet.options.enableCollapse} option).
     */
    numberOfValues: ComponentOptions.buildNumberOption({ min: 0, defaultValue: 8, section: 'CommonOptions' }),

    /**
     * Whether to allow the end-user to expand and collapse this facet.
     */
    enableCollapse: ComponentOptions.buildBooleanOption({ defaultValue: true, section: 'CommonOptions' }),

    /**
     * Whether to scroll back to the top of the page whenever the end-user interacts with the facet.
     */
    enableScrollToTop: ComponentOptions.buildBooleanOption({ defaultValue: true, section: 'CommonOptions' }),

    /**
     * Whether to enable the **Show more** and **Show less** buttons in the facet.
     *
     * **Note:** The [`DynamicFacetRange`]{@link DynamicFacetRange} component does not support this option.
     */
    enableMoreLess: ComponentOptions.buildBooleanOption({ defaultValue: true, section: 'CommonOptions' }),

    /**
     * Whether to allow the end-user to search the facet values.
     *
     * **Note:** The [`DynamicFacetRange`]{@link DynamicFacetRange} component does not support this option.
     *
     * By default, the following behavior applies:
     *
     * - Enabled when more facet values are available.
     * - Disabled when all available facet values are already displayed.
     */
    enableFacetSearch: ComponentOptions.buildBooleanOption({ section: 'Filtering' }),

    /**
     * Whether to prepend facet search queries with a wildcard.
     *
     * **Note:** The [`DynamicFacetRange`]{@link DynamicFacetRange} component does not support this option.
     */
    useLeadingWildcardInFacetSearch: ComponentOptions.buildBooleanOption({
      defaultValue: true,
      section: 'Filtering',
      depend: 'enableFacetSearch'
    }),

    /**
     * Whether this facet should be collapsed by default.
     */
    collapsedByDefault: ComponentOptions.buildBooleanOption({ defaultValue: false, section: 'CommonOptions', depend: 'enableCollapse' }),

    /**
     * Whether to notify the [`Breadcrumb`]{@link Breadcrumb} component when toggling values in the facet.
     *
     * See also the [`numberOfValuesInBreadcrumb`]{@link DynamicFacet.options.numberOfValuesInBreadcrumb} option.
     */
    includeInBreadcrumb: ComponentOptions.buildBooleanOption({ defaultValue: true, section: 'CommonOptions' }),

    /**
     * The maximum number of selected values the [`Breadcrumb`]{@link Breadcrumb} component can display before outputting a **N more...** link for the facet.
     */
    numberOfValuesInBreadcrumb: ComponentOptions.buildNumberOption({
      defaultFunction: () => (DeviceUtils.isMobileDevice() ? 3 : 5),
      min: 0,
      depend: 'includeInBreadcrumb',
      section: 'CommonOptions'
    }),

    /**
     * A mapping of facet values to their desired captions.
     *
     * **Note:** The [`DynamicFacetRange`]{@link DynamicFacetRange} component does not support this option.
     *
     * @externaldocs [Normalizing Facet Value Captions](https://docs.coveo.com/368/).
     * @examples { "smith_alice": "Alice Smith"\, "jones_bob_r": "Bob R. Jones" }
     */
    valueCaption: ComponentOptions.buildJsonOption<IStringMap<string>>({ defaultValue: {} }),

    /**
     * The [`id`]{@link DynamicFacet.options.id} of another facet in which at least one value must be selected in order for the dependent facet to be visible.
     *
     * By default, the facet does not depend on any other facet to be displayed.
     *
     * @examples document-type-facet
     *
     * @availablesince [December 2019 Release (v2.7610)](https://docs.coveo.com/en/3142/)
     */
    dependsOn: ComponentOptions.buildStringOption({ section: 'CommonOptions' }),

    /**
     * A function that verifies whether the current state of the `dependsOn` facet allows the dependent facet to be displayed.
     *
     * If specified, the function receives a reference to the resolved `dependsOn` facet component instance as an argument, and must return a boolean.
     * The function's argument should typically be treated as read-only.
     *
     * By default, the dependent facet is displayed whenever one or more values are selected in its `dependsOn` facet.
     *
     * @externaldocs [Defining Dependent Facets](https://docs.coveo.com/3210/)
     *
     * @availablesince [April 2020 Release (v2.8864)](https://docs.coveo.com/en/3231/)
     */
    dependsOnCondition: ComponentOptions.buildCustomOption<IDependentFacetCondition>(
      () => {
        return null;
      },
      { depend: 'dependsOn', section: 'CommonOptions' }
    ),

    /**
     * The number of items to scan for facet values.
     *
     * Setting this option to a higher value may enhance the accuracy of facet value counts at the cost of slower query performance.
     *
     * @availablesince [January 2020 Release (v2.7968)](https://docs.coveo.com/en/3163/)
     *
     * @examples 1500
     */
    injectionDepth: ComponentOptions.buildNumberOption({ defaultValue: 1000, min: 1000 }),

    /**
     * Whether to exclude folded result parents when estimating result counts for facet values.
     *
     * **Note:** The target folding field must be a facet field with the **Use cache for nested queries** options enabled (see [Add or Edit a Field](https://docs.coveo.com/en/1982)).
     *
     * See also the [`Folding`]{@link Folding} and [`FoldingForThread`]{@link FoldingForThread} components.
     *
     * **Default:** `true`.
     *
     * @availablesince [March 2020 Release (v2.8521)](https://docs.coveo.com/en/3203/)
     */
    filterFacetCount: ComponentOptions.buildBooleanOption({ section: 'Filtering' }),

    /**
     * The [heading level](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Heading_Elements) to use for the heading above the facet.
     *
     * A value of 0 will render a [`div`](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/div) element instead.
     *
     * **Default:** `2`.
     */
    headingLevel: ComponentOptions.buildNumberOption({ defaultValue: 2, min: 0, max: 6 })
  };

  private includedAttributeId: string;
  private listenToQueryStateChange = true;
  private search: DynamicFacetSearch;
  private valueToFocusOnRender: string | null;
  public header: DynamicFacetHeader;

  public options: IDynamicFacetOptions;
  public dynamicFacetManager: DynamicFacetManager;
  public dependsOnManager: DependsOnManager;
  public dynamicFacetQueryController: DynamicFacetQueryController;
  public values: DynamicFacetValues;
  public position: number;
  public moreValuesAvailable = false;
  public isCollapsed: boolean;
  public isDynamicFacet = true;
  public isFieldValueCompatible = true;
  public isFieldValueHierarchical = false;

  /**
   * Creates a new `DynamicFacet` instance.
   *
   * @param element The element from which to instantiate the component.
   * @param options The component options.
   * @param bindings The component bindings. Automatically resolved by default.
   */
  constructor(
    public element: HTMLElement,
    options?: IDynamicFacetOptions,
    bindings?: IComponentBindings,
    classId: string = DynamicFacet.ID
  ) {
    super(element, classId, bindings);
    this.options = ComponentOptions.initComponentOptions(element, DynamicFacet, options);

    this.initDynamicFacetQueryController();
    this.initDependsOnManager();
    this.initQueryEvents();
    this.initQueryStateEvents();
    this.initBreadCrumbEvents();
    this.initComponentStateEvents();
    this.initValues();
    this.verifyCollapsingConfiguration();
    this.isCollapsed = this.options.enableCollapse && this.options.collapsedByDefault;

    ResponsiveDynamicFacets.init(this.root, this, this.options);
  }

  public get fieldName() {
    return this.options.field.slice(1);
  }

  public get facetType() {
    return FacetType.specific;
  }

  /**
   * Selects a single value in this facet.
   *
   * Does **not** trigger a query automatically.
   * Does **not** update the visual of the facet until a query is performed.
   *
   * @param value The name of the facet value to select.
   */
  public selectValue(value: string) {
    Assert.exists(value);
    this.selectMultipleValues([value]);
  }

  /**
   * Selects multiple values in this facet.
   *
   * Does **not** trigger a query automatically.
   * Does **not** update the visual of the facet until a query is performed.
   *
   * @param values The names of the facet values to select.
   */
  public selectMultipleValues(values: string[]) {
    Assert.exists(values);
    this.ensureDom();
    this.logger.info('Selecting facet value(s)', values);
    values.forEach(value => {
      this.values.get(value).select();
    });
    this.updateQueryStateModel();
  }

  /**
   * Deselects a single value in this facet.
   *
   * Does **not** trigger a query automatically.
   * Does **not** update the visual of the facet until a query is performed.
   *
   * @param values The name of the facet value to deselect.
   */
  public deselectValue(value: string) {
    Assert.exists(value);
    this.deselectMultipleValues([value]);
  }

  /**
   * Determines whether the specified value is selected in the facet.
   * @param value The name of the facet value to verify.
   */
  public hasSelectedValue(value: string) {
    return this.values.hasSelectedValue(value);
  }

  /**
   * Deselects multiple values in this facet.
   *
   * Does **not** trigger a query automatically.
   * Does **not** update the visual of the facet until a query is performed.
   *
   * @param values The names of the facet values to deselect.
   */
  public deselectMultipleValues(values: string[]) {
    Assert.exists(values);
    this.ensureDom();
    this.logger.info('Deselecting facet value(s)', values);
    values.forEach(value => {
      this.values.get(value).deselect();
    });
    this.updateQueryStateModel();
  }

  /**
   * Toggles the selection state of a single value in this facet.
   *
   * Does **not** trigger a query automatically.
   *
   * @param values The name of the facet value to toggle.
   */
  public toggleSelectValue(value: string) {
    Assert.exists(value);
    this.ensureDom();
    const facetValue = this.values.get(value);
    facetValue.toggleSelect();
    this.logger.info('Toggle select facet value', facetValue);
    this.updateQueryStateModel();
  }

  /**
   * Keyboard focuses a value once it has been re-rendered.
   *
   * @param value The name of the facet value to focus
   */
  public focusValueAfterRerender(value: string) {
    Assert.exists(value);
    this.valueToFocusOnRender = value;
  }

  /**
   * Returns the configured caption for a desired facet value.
   *
   * @param value The string facet value whose caption the method should return.
   */
  public getCaptionForStringValue(value: string) {
    return FacetUtils.getDisplayValueFromValueCaption(value, this.options.field as string, this.options.valueCaption);
  }

  /**
   * Requests additional values.
   *
   * Automatically triggers an isolated query.
   * @param additionalNumberOfValues The number of additional values to request. Minimum value is 1. Defaults to the [numberOfValues]{@link DynamicFacet.options.numberOfValues} option value.
   */
  public showMoreValues(additionalNumberOfValues = this.options.numberOfValues) {
    this.ensureDom();
    this.logger.info('Show more values');
    this.dynamicFacetQueryController.increaseNumberOfValuesToRequest(additionalNumberOfValues);
    this.triggerNewIsolatedQuery(() => this.logAnalyticsFacetShowMoreLess(analyticsActionCauseList.dynamicFacetShowMore));
  }

  /**
   * Reduces the number of displayed facet values down to [numberOfValues]{@link DynamicFacet.options.numberOfValues}.
   *
   * Automatically triggers an isolated query.
   */
  public showLessValues() {
    this.ensureDom();
    this.logger.info('Show less values');
    this.dynamicFacetQueryController.resetNumberOfValuesToRequest();
    this.triggerNewIsolatedQuery(() => this.logAnalyticsFacetShowMoreLess(analyticsActionCauseList.dynamicFacetShowLess));
  }

  /**
   * Deselects all values in this facet.
   *
   * Does **not** trigger a query automatically.
   * Updates the visual of the facet.
   *
   */
  public reset() {
    this.ensureDom();
    if (!this.values.hasActiveValues) {
      return;
    }

    this.logger.info('Deselect all values');
    this.values.clearAll();
    this.values.render();
    this.enablePreventAutoSelectionFlag();
    this.updateAppearance();
    this.updateQueryStateModel();
  }

  /**
   * Collapses or expands the facet depending on it's current state.
   */
  public toggleCollapse() {
    this.isCollapsed ? this.expand() : this.collapse();
  }

  /**
   * Expands the facet, displaying all of its currently fetched values.
   */
  public expand() {
    if (!this.options.enableCollapse) {
      return this.logger.warn(`Calling expand() won't do anything on a facet that has the option "enableCollapse" set to "false"`);
    }
    if (!this.isCollapsed) {
      return;
    }
    this.ensureDom();
    this.logger.info('Expand facet values');
    this.isCollapsed = false;
    this.updateAppearance();
  }

  /**
   * Collapses the facet, displaying only its currently selected values.
   */
  public collapse() {
    if (!this.options.enableCollapse) {
      return this.logger.warn(`Calling collapse() won't do anything on a facet that has the option "enableCollapse" set to "false"`);
    }
    if (this.isCollapsed) {
      return;
    }
    this.ensureDom();
    this.logger.info('Collapse facet values');
    this.isCollapsed = true;
    this.updateAppearance();
  }

  /**
   * Sets a flag indicating whether the facet values should be returned in their current order.
   *
   * Setting the flag to `true` helps ensuring that the values do not move around while the end-user is interacting with them.
   *
   * The flag is automatically set back to `false` after a query is built.
   */
  public enableFreezeCurrentValuesFlag() {
    this.dynamicFacetQueryController.enableFreezeCurrentValuesFlag();
  }

  /**
   * For this method to work, the component has to be the child of a [DynamicFacetManager]{@link DynamicFacetManager} component.
   *
   * Sets a flag indicating whether the facets should be returned in their current order.
   *
   * Setting the flag to `true` helps ensuring that the facets do not move around while the end-user is interacting with them.
   *
   * The flag is automatically set back to `false` after a query is built.
   */
  public enableFreezeFacetOrderFlag() {
    this.dynamicFacetQueryController.enableFreezeFacetOrderFlag();
  }

  public enablePreventAutoSelectionFlag() {
    this.dynamicFacetQueryController.enablePreventAutoSelectionFlag();
  }

  public scrollToTop() {
    if (this.options.enableScrollToTop) {
      ResultListUtils.scrollToTop(this.root);
    }
  }

  public get analyticsFacetState(): IAnalyticsFacetState[] {
    return this.values.activeValues.map(facetValue => facetValue.analyticsFacetState);
  }

  public get basicAnalyticsFacetState(): IAnalyticsFacetState {
    return {
      field: this.options.field.toString(),
      id: this.options.id,
      title: this.options.title,
      facetType: this.facetType,
      facetPosition: this.position
    };
  }

  public get basicAnalyticsFacetMeta(): IAnalyticsFacetMeta {
    return {
      facetField: this.options.field.toString(),
      facetId: this.options.id,
      facetTitle: this.options.title
    };
  }

  public logAnalyticsEvent(actionCause: IAnalyticsActionCause, facetMeta: IAnalyticsFacetMeta) {
    this.usageAnalytics.logSearchEvent<IAnalyticsFacetMeta>(actionCause, facetMeta);
  }

  public putStateIntoQueryBuilder(queryBuilder: QueryBuilder) {
    Assert.exists(queryBuilder);
    this.dynamicFacetQueryController.putFacetIntoQueryBuilder(queryBuilder);
  }

  public putStateIntoAnalytics() {
    const pendingEvent = this.usageAnalytics.getPendingSearchEvent();
    pendingEvent && pendingEvent.addFacetState(this.analyticsFacetState);
  }

  public isCurrentlyDisplayed() {
    return $$(this.element).isVisible();
  }

  public get hasActiveValues() {
    return this.values.hasActiveValues;
  }

  private initQueryEvents() {
    this.bind.onRootElement(QueryEvents.duringQuery, () => this.ensureDom());
    this.bind.onRootElement(QueryEvents.doneBuildingQuery, (data: IDoneBuildingQueryEventArgs) => this.handleDoneBuildingQuery(data));
    this.bind.onRootElement(QueryEvents.deferredQuerySuccess, (data: IQuerySuccessEventArgs) => this.handleQuerySuccess(data.results));
    this.bind.onRootElement(QueryEvents.queryError, () => this.onNoValues());
  }

  private initQueryStateEvents() {
    this.includedAttributeId = QueryStateModel.getFacetId(this.options.id);
    this.queryStateModel.registerNewAttribute(this.includedAttributeId, []);
    this.bind.onQueryState(MODEL_EVENTS.CHANGE, undefined, this.handleQueryStateChanged);
  }

  protected initBreadCrumbEvents() {
    if (this.options.includeInBreadcrumb) {
      this.bind.onRootElement(BreadcrumbEvents.populateBreadcrumb, (args: IPopulateBreadcrumbEventArgs) =>
        this.handlePopulateBreadcrumb(args)
      );
      this.bind.onRootElement(BreadcrumbEvents.clearBreadcrumb, () => this.reset());
    }
  }

  protected initValues() {
    this.values = new DynamicFacetValues(this, DynamicFacetValueCreator);
  }

  private initComponentStateEvents() {
    const componentStateId = QueryStateModel.getFacetId(this.options.id);
    this.componentStateModel.registerComponent(componentStateId, this);
  }

  protected initDynamicFacetQueryController() {
    this.dynamicFacetQueryController = new DynamicFacetQueryController(this);
  }

  private handleDoneBuildingQuery(data: IDoneBuildingQueryEventArgs) {
    // If there is a DynamicFacetManager, it will take care of adding the facet's state
    if (this.dynamicFacetManager) {
      return;
    }

    Assert.exists(data);
    Assert.exists(data.queryBuilder);
    this.putStateIntoQueryBuilder(data.queryBuilder);
    this.putStateIntoAnalytics();
  }

  private handleQuerySuccess(results: IQueryResults) {
    // If there is a DynamicFacetManager, it will take care of handling the results
    if (this.dynamicFacetManager) {
      return;
    }

    if (Utils.isNullOrUndefined(results.facets)) {
      return this.notImplementedError();
    }

    this.handleQueryResults(results);
  }

  public handleQueryResults(results: IQueryResults) {
    const index = findIndex(results.facets, { facetId: this.options.id });
    const facetResponse = index !== -1 ? results.facets[index] : null;

    this.position = facetResponse ? index + 1 : undefined;
    facetResponse ? this.onNewValues(facetResponse) : this.onNoValues();

    this.header.hideLoading();
    this.updateQueryStateModel();
    this.values.render();
    this.updateAppearance();
    if (this.valueToFocusOnRender) {
      const value = this.valueToFocusOnRender;
      this.valueToFocusOnRender = null;
      defer(() => this.values.focus(value));
    }
  }

  private onNewValues(facetResponse: IFacetResponse) {
    this.moreValuesAvailable = facetResponse.moreValuesAvailable;
    this.values.createFromResponse(facetResponse);
    if (this.options.customSort) {
      this.values.reorderValues(this.options.customSort);
    }
  }

  private onNoValues() {
    this.moreValuesAvailable = false;
    this.values.resetValues();
  }

  private handleQueryStateChanged(data: IAttributesChangedEventArg) {
    if (!this.listenToQueryStateChange) {
      return;
    }

    const querySelectedValues: string[] = data.attributes[this.includedAttributeId];
    if (!querySelectedValues) {
      return;
    }

    this.handleQueryStateChangedIncluded(querySelectedValues);
  }

  private handleQueryStateChangedIncluded = (querySelectedValues: string[]) => {
    const currentSelectedValues = this.values.selectedValues;
    const validQuerySelectedValues = querySelectedValues.filter(value => this.values.get(value));

    const valuesToSelect = difference(validQuerySelectedValues, currentSelectedValues);
    const valuesToDeselect = difference(currentSelectedValues, validQuerySelectedValues);

    if (Utils.isNonEmptyArray(valuesToSelect)) {
      this.selectMultipleValues(valuesToSelect);
    }

    if (Utils.isNonEmptyArray(valuesToDeselect)) {
      this.deselectMultipleValues(valuesToDeselect);
    }
  };

  private handlePopulateBreadcrumb(args: IPopulateBreadcrumbEventArgs) {
    Assert.exists(args);

    if (!this.values.hasActiveValues) {
      return;
    }

    const breadcrumbs = new DynamicFacetBreadcrumbs(this, { headingLevel: args.headingLevel });
    args.breadcrumbs.push({ element: breadcrumbs.element });
  }

  private initDependsOnManager() {
    const facetInfo: IDependentFacet = {
      reset: () => this.reset(),
      ref: this
    };

    this.dependsOnManager = new DependsOnManager(facetInfo);
  }

  public createDom() {
    this.createAndAppendContent();
    this.updateAppearance();
  }

  private createAndAppendContent() {
    this.createAndAppendHeader();
    this.createAndAppendSearch();
    this.createAndAppendValues();
  }

  private createAndAppendHeader() {
    this.header = new DynamicFacetHeader({
      id: this.options.id,
      title: this.options.title,
      enableCollapse: this.options.enableCollapse,
      headingLevel: this.options.headingLevel,
      clear: () => this.clear(),
      toggleCollapse: () => this.toggleCollapse(),
      collapse: () => this.collapse(),
      expand: () => this.expand()
    });
    this.element.appendChild(this.header.element);
  }

  private createAndAppendSearch() {
    if (this.options.enableFacetSearch === false) {
      return;
    }

    this.search = new DynamicFacetSearch(this);
    this.element.appendChild(this.search.element);
  }

  private createAndAppendValues() {
    this.element.appendChild(this.values.render());
  }

  private updateQueryStateModel() {
    this.listenToQueryStateChange = false;
    this.queryStateModel.set(this.includedAttributeId, this.values.selectedValues);
    this.listenToQueryStateChange = true;
  }

  private updateAppearance() {
    this.header.toggleClear(this.values.hasSelectedValues);
    this.header.toggleCollapse(this.isCollapsed);
    this.toggleSearchDisplay();
    $$(this.element).toggleClass('coveo-dynamic-facet-collapsed', this.isCollapsed);
    $$(this.element).toggleClass('coveo-active', this.values.hasSelectedValues);
    $$(this.element).toggleClass('coveo-hidden', !this.values.hasDisplayedValues);
  }

  private toggleSearchDisplay() {
    if (this.options.enableFacetSearch === false) {
      return;
    }

    if (this.isCollapsed) {
      return $$(this.search.element).toggleClass('coveo-hidden', true);
    }

    $$(this.search.element).toggleClass('coveo-hidden', !this.options.enableFacetSearch && !this.moreValuesAvailable);
  }

  public triggerNewQuery(beforeExecuteQuery?: () => void) {
    this.beforeSendingQuery();

    const options: IQueryOptions = beforeExecuteQuery ? { beforeExecuteQuery } : { ignoreWarningSearchEvent: true };

    this.queryController.executeQuery(options);
  }

  public async triggerNewIsolatedQuery(beforeExecuteQuery?: () => void) {
    this.beforeSendingQuery();
    beforeExecuteQuery && beforeExecuteQuery();

    try {
      const results = await this.dynamicFacetQueryController.getQueryResults();
      this.handleQueryResults(results);
    } catch (e) {
      this.header.hideLoading();
    }
  }

  private beforeSendingQuery() {
    this.header.showLoading();
  }

  private notImplementedError() {
    this.logger.error('DynamicFacets are not supported by your current search endpoint. Disabling this component.');
    this.disable();
    this.updateAppearance();
  }

  private verifyCollapsingConfiguration() {
    if (this.options.collapsedByDefault && !this.options.enableCollapse) {
      this.logger.warn('The "collapsedByDefault" option is "true" while the "enableCollapse" is "false"');
    }
  }

  private logAnalyticsFacetShowMoreLess(cause: IAnalyticsActionCause) {
    this.usageAnalytics.logCustomEvent<IAnalyticsFacetMeta>(cause, this.basicAnalyticsFacetMeta, this.element);
  }

  private clear() {
    this.reset();
    this.enableFreezeFacetOrderFlag();
    this.scrollToTop();
    this.triggerNewQuery(() => this.logClearAllToAnalytics());
  }

  private logClearAllToAnalytics() {
    this.logAnalyticsEvent(analyticsActionCauseList.dynamicFacetClearAll, this.basicAnalyticsFacetMeta);
  }
}

Initialization.registerAutoCreateComponent(DynamicFacet);
DynamicFacet.doExport();
