import 'styling/DynamicFacet/_DynamicFacet';
import { Component } from '../Base/Component';
import { l } from '../../strings/Strings';
import { ComponentOptions } from '../Base/ComponentOptions';
import { IComponentBindings } from '../Base/ComponentBindings';
import { $$ } from '../../utils/Dom';
import { Initialization } from '../Base/Initialization';
import { exportGlobally } from '../../GlobalExports';
import { DynamicHierarchicalFacetQueryController } from '../../controllers/DynamicHierarchicalFacetQueryController';
import { QueryStateModel } from '../../models/QueryStateModel';
import { IAttributesChangedEventArg, MODEL_EVENTS } from '../../models/Model';
import { Utils } from '../../utils/Utils';
import { isArray, findIndex, last, isEqual } from 'underscore';
import { Assert } from '../../misc/Assert';
import { QueryEvents, IQuerySuccessEventArgs, IDoneBuildingQueryEventArgs } from '../../events/QueryEvents';
import { BreadcrumbEvents, IPopulateBreadcrumbEventArgs } from '../../events/BreadcrumbEvents';
import { DynamicHierarchicalFacetBreadcrumb } from './DynamicHierarchicalFacetBreadcrumb';
import { analyticsActionCauseList, IAnalyticsActionCause, IAnalyticsFacetMeta } from '../Analytics/AnalyticsActionListMeta';
import { QueryBuilder } from '../Base/QueryBuilder';
import { ResponsiveFacets } from '../ResponsiveComponents/ResponsiveFacets';
import {
  IDynamicHierarchicalFacetOptions,
  IDynamicHierarchicalFacet,
  IDynamicHierarchicalFacetValues,
  HierarchicalFacetSortCriteria
} from './IDynamicHierarchicalFacet';
import { ResponsiveFacetOptions } from '../ResponsiveComponents/ResponsiveFacetOptions';
import { DynamicFacetHeader } from '../DynamicFacet/DynamicFacetHeader/DynamicFacetHeader';
import { IStringMap } from '../../rest/GenericParam';
import { DependsOnManager, IDependentFacet, IDependentFacetCondition } from '../../utils/DependsOnManager';
import { ResultListUtils } from '../../utils/ResultListUtils';
import { FacetType } from '../../rest/Facet/FacetRequest';
import { DynamicHierarchicalFacetValues } from './DynamicHierarchicalFacetValues/DynamicHierarchicalFacetValues';
import { IFacetResponse } from '../../rest/Facet/FacetResponse';
import { IQueryOptions } from '../../controllers/QueryController';
import { IQueryResults } from '../../rest/QueryResults';
import { DynamicFacetManager } from '../DynamicFacetManager/DynamicFacetManager';
import { IAnalyticsFacetState } from '../Analytics/IAnalyticsFacetState';
import { FacetValueState } from '../../rest/Facet/FacetValueState';
import { FacetSortCriteria } from '../../rest/Facet/FacetSortCriteria';
import { Logger } from '../../misc/Logger';
import { DynamicHierarchicalFacetSearch } from '../DynamicHierarchicalFacetSearch/DynamicHierarchicalFacetSearch';
import { IFieldValueCompatibleFacet } from '../FieldValue/IFieldValueCompatibleFacet';

/**
 * The `DynamicHierarchicalFacet` component is a facet that renders values in a hierarchical fashion. It determines the filter to apply depending on the
 * selected path of values.
 *
 * This facet requires a [`field`]{@link DynamicHierarchicalFacet.options.field} with a special format to work correctly.
 *
 * @notSupportedIn salesforcefree
 * @availablesince [January 2020 Release (v2.7968)](https://docs.coveo.com/en/3163/)
 * @externaldocs [Using Hierarchical Facets](https://docs.coveo.com/en/2667)
 */
export class DynamicHierarchicalFacet extends Component implements IDynamicHierarchicalFacet, IFieldValueCompatibleFacet {
  static ID = 'DynamicHierarchicalFacet';
  static doExport = () => {
    exportGlobally({
      DynamicHierarchicalFacet
    });
  };

  /**
   * The options for the DynamicHierarchicalFacet
   * @componentOptions
   */
  static options: IDynamicHierarchicalFacetOptions = {
    /**
     * A unique identifier for the facet. Among other things, this identifier serves the purpose of saving
     * the facet state in the URL hash.
     *
     * If you have two facets with the same field on the same page, you should specify an `id` value for at least one of
     * those two facets. This `id` must be unique among the facets.
     *
     * Default value is the [`field`]{@link DynamicHierarchicalFacet.options.field} option value.
     *
     * @examples productcategory
     */
    id: ComponentOptions.buildStringOption({
      postProcessing: (value, options: IDynamicHierarchicalFacetOptions) => value || (options.field as string),
      section: 'CommonOptions'
    }),

    /**
     * The index field whose values the facet should use. The field values should have the form:
     * `the; the|path; the|path|to; the|path|to|given; the|path|to|given|item;`
     * where the delimiting character is `|`. This default delimiting character can be changed using the [delimitingCharacter]{@link DynamicHierarchicalFacet.options.delimitingCharacter} option.
     *
     * To help you verify whether your fields are setup correctly, use the [`debugInfo`]{@link DynamicHierarchicalFacet.debugInfo} method.
     *
     * @externaldocs [Using the Hierarchical Facet Component](https://docs.coveo.com/en/2667).
     * @examples @category
     */
    field: ComponentOptions.buildFieldOption({ required: true, section: 'CommonOptions' }),

    /**
     * The title to display at the top of the facet.
     *
     * Default value is the localized string for `NoTitle`.
     *
     * @examples Product category
     */
    title: ComponentOptions.buildLocalizedStringOption({
      localizedString: () => l('NoTitle'),
      section: 'CommonOptions'
    }),

    /**
     * The maximum number of field values to display by default in the facet before the user
     * clicks the arrow to show more.
     *
     * See also the [`enableMoreLess`]{@link DynamicHierarchicalFacet.options.enableMoreLess} option.
     *
     * @examples 8
     */
    numberOfValues: ComponentOptions.buildNumberOption({ defaultValue: 5, min: 0, section: 'CommonOptions' }),

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

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

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

    /**
     * Whether to notify the [`Breadcrumb`]{@link Breadcrumb} component when toggling values in the facet.
     */
    includeInBreadcrumb: ComponentOptions.buildBooleanOption({ defaultValue: true, 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.
     *
     * @examples 1500
     */
    injectionDepth: ComponentOptions.buildNumberOption({ defaultValue: 1000, min: 1000 }),

    /**
     * Whether to enable the **More** and **Less** buttons in the Facet.
     *
     * See also the [`numberOfValues`]{@link DynamicHierarchicalFacet.options.numberOfValues} option.
     */
    enableMoreLess: ComponentOptions.buildBooleanOption({ defaultValue: true, section: 'CommonOptions' }),

    /**
     * Whether to allow the end-user to search the facet values.
     */
    enableFacetSearch: ComponentOptions.buildBooleanOption({ section: 'Filtering', defaultValue: false }),

    /**
     * The character that specifies the hierarchical dependency.
     *
     * **Example:**
     *
     * If your field has the following values:
     *
     * `electronics; electronics>laptops; electronics>laptops>gaming;`
     *
     * The delimiting character is `>`.
     *
     * @examples >
     */
    delimitingCharacter: ComponentOptions.buildStringOption({ defaultValue: '|', section: 'CommonOptions' }),

    /**
     * A mapping of facet values to their desired captions.
     *
     * @externaldocs [Normalizing Facet Value Captions](https://docs.coveo.com/en/368/)
     */
    valueCaption: ComponentOptions.buildJsonOption<IStringMap<string>>({ defaultValue: {} }),

    /**
     * The `id` option value of another facet in which at least one value must be selected in order
     * for the dependent hierarchical facet to be visible.
     *
     * **Default:** `undefined` and the hierarchical facet does not depend on any other facet to be displayed.
     *
     * @externaldocs [Defining Dependent Facets](https://docs.coveo.com/3210/)
     * @examples department
     */
    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/)
     */
    dependsOnCondition: ComponentOptions.buildCustomOption<IDependentFacetCondition>(
      () => {
        return null;
      },
      { depend: 'dependsOn', section: 'CommonOptions' }
    ),

    /**
     * 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`.
     */
    filterFacetCount: ComponentOptions.buildBooleanOption({ section: 'Filtering' }),

    /**
     * The sort criterion to use for this facet.
     *
     * **Default (Search API):** `occurrences`.
     *
     * @examples alphanumeric
     *
     * @availablesince [March 2020 Release (v2.8521)](https://docs.coveo.com/en/3203/)
     */
    sortCriteria: <HierarchicalFacetSortCriteria>ComponentOptions.buildStringOption({
      postProcessing: value => {
        if (!value) {
          return undefined;
        }

        if (value === FacetSortCriteria.alphanumeric || value === FacetSortCriteria.occurrences) {
          return value;
        }

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

    /**
     * Specifies a custom order by which to sort the dynamic hierarchical facet values.
     *
     * Custom-ordered values won't necessarily retrieve values from the index.
     *
     * **Example:**
     * ```html
     * <div
     *   class="CoveoDynamicHierarchicalFacet"
     *   data-field="@countries"
     *   data-title="Countries"
     *   data-tab="All"
     *   data-clear-label="All Countries"
     *   data-delimiting-character="|"
     *   data-custom-sort="America, Asia|Japan, America|Mexico, America|Canada, Europe|Germany, Europe|France, Oceania"
     * ></div>
     * ```
     */
    customSort: ComponentOptions.buildStringOption({ section: 'Sorting' }),

    /**
     * If a custom sort is defined, this specifies the delimiter that separates paths from each other.
     *
     * *Example:**
     * ```html
     * <div
     *   class="CoveoDynamicHierarchicalFacet"
     *   data-field="@countries"
     *   data-title="Countries"
     *   data-tab="All"
     *   data-clear-label="All Countries"
     *   data-delimiting-character="|"
     *   data-custom-sort="America...Asia|Japan...America|Mexico...America|Canada...Europe|Germany...Europe|France...Oceania"
     *   data-custom-sort-delimiting-character="..."
     * ></div>
     * ```
     */
    customSortDelimitingCharacter: ComponentOptions.buildStringOption({
      section: 'Sorting',
      depend: 'customSort',
      postProcessing: (value, options: IDynamicHierarchicalFacetOptions) =>
        Utils.isNullOrUndefined(value) ? (options.delimitingCharacter === ',' ? '|' : ',') : value
    }),

    /**
     * The label to display to clear the facet when a value is selected.
     *
     * Default value is the localized string for `AllCategories`.
     *
     * @examples Everything
     */
    clearLabel: ComponentOptions.buildLocalizedStringOption({
      localizedString: () => l('AllCategories'),
      section: 'CommonOptions'
    }),

    /**
     * The base path shared by all values to display in the hierarchical facet.
     *
     * If you set this option, the specified base path will always be active on the facet.
     *
     * This implies that:
     * - The end user will not have to manually select the specified base path values.
     * - Values that do not have the specified base path will not be displayed in the facet.
     *
     * @examples electronics, electronics\,laptops
     *
     * @availablesince [April 2020 Release (v2.8864)](https://docs.coveo.com/en/3231/)
     */
    basePath: ComponentOptions.buildListOption<string>({ defaultValue: [] }),

    /**
     * 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 }),

    ...ResponsiveFacetOptions
  };

  private listenToQueryStateChange = true;
  private search: DynamicHierarchicalFacetSearch;

  public options: IDynamicHierarchicalFacetOptions;
  public dynamicHierarchicalFacetQueryController: DynamicHierarchicalFacetQueryController;
  public dependsOnManager: DependsOnManager;
  public isCollapsed: boolean;
  public header: DynamicFacetHeader;
  public values: IDynamicHierarchicalFacetValues;
  public moreValuesAvailable = false;
  public position: number;
  public dynamicFacetManager: DynamicFacetManager;
  public isDynamicFacet = true;
  public isFieldValueCompatible = true;
  public isFieldValueHierarchical = true;

  constructor(public element: HTMLElement, options: IDynamicHierarchicalFacetOptions, bindings?: IComponentBindings) {
    super(element, 'DynamicHierarchicalFacet', bindings);
    this.options = ComponentOptions.initComponentOptions(element, DynamicHierarchicalFacet, options);

    this.dynamicHierarchicalFacetQueryController = new DynamicHierarchicalFacetQueryController(this);
    this.isCollapsed = this.options.enableCollapse && this.options.collapsedByDefault;
    this.verifyCollapsingConfiguration();
    this.values = new DynamicHierarchicalFacetValues(this);

    ResponsiveFacets.init(this.root, this, this.options);
    this.initDependsOnManager();
    this.initBreadCrumbEvents();
    this.initQueryEvents();
    this.initQueryStateEvents();
  }

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

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

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

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

  public hasSelectedValue(value: string) {
    const values = this.splitValue(value);
    return isEqual(values, this.values.selectedPath);
  }

  public selectValue(value: string) {
    this.selectPath(this.splitValue(value));
  }

  public deselectValue() {
    this.clear();
  }

  public getCaptionForStringValue(value: string) {
    return this.getCaption(last(this.splitValue(value)));
  }

  private initQueryEvents() {
    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.duringQuery, () => this.ensureDom());
    this.bind.onRootElement(QueryEvents.queryError, () => this.onNoValues());
  }

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

  private get queryStateAttribute() {
    return QueryStateModel.getFacetId(this.options.id);
  }

  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 verifyCollapsingConfiguration() {
    if (this.options.collapsedByDefault && !this.options.enableCollapse) {
      this.logger.warn('Setting "collapseByDefault" to "true" has no effect when "enableCollapse" is set to "false"');
    }
  }

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

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

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

  private updateAppearance() {
    this.header.toggleCollapse(this.isCollapsed);
    this.toggleSearchDisplay();
    $$(this.element).toggleClass('coveo-dynamic-hierarchical-facet-collapsed', this.isCollapsed);
    $$(this.element).toggleClass('coveo-hidden', !this.values.allFacetValues.length);
  }

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

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

  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.selectedPath);
    this.values.render();
    this.updateAppearance();
  }

  private splitValue(value: string) {
    return value.trim().split(this.options.delimitingCharacter);
  }

  private onNewValues(facetResponse: IFacetResponse) {
    this.moreValuesAvailable = facetResponse.moreValuesAvailable;
    this.values.createFromResponse(facetResponse);
    if (this.options.customSort) {
      const order = this.options.customSort.split(this.options.customSortDelimitingCharacter).map(value => this.splitValue(value));
      this.values.reorderValues(order);
    }
  }

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

  private updateQueryStateModel(path: string[]) {
    this.listenToQueryStateChange = false;
    this.queryStateModel.set(this.queryStateAttribute, path);
    this.listenToQueryStateChange = true;
  }

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

  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.dynamicHierarchicalFacetQueryController.getQueryResults();
      this.handleQueryResults(results);
    } catch (e) {
      this.header.hideLoading();
    }
  }

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

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

  /**
   * Select a path in the hierarchy.
   *
   * Does **not** trigger a query automatically.
   * Does **not** update the visual of the facet until a query is performed.
   *
   * @param path The values representing the path.
   */
  public selectPath(path: string[]) {
    Assert.exists(path);
    Assert.isLargerThan(0, path.length);
    this.ensureDom();
    this.updateQueryStateModel(path);
    this.values.selectPath(path);
    this.logger.info('Toggle select facet value at path', path);
  }

  private clear() {
    this.reset();
    this.scrollToTop();
    this.triggerNewQuery(() => this.logAnalyticsEvent(analyticsActionCauseList.dynamicFacetClearAll));
  }

  /**
   * Deselects the path in the facet.
   *
   * Does **not** trigger a query automatically.
   * Does **not** update the visual of the facet until a query is performed.
   */
  public reset() {
    this.ensureDom();

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

    this.enablePreventAutoSelectionFlag();
    this.logger.info('Deselect facet value');
    this.values.clearPath();
    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();
  }

  /**
   * 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.dynamicHierarchicalFacetQueryController.enableFreezeFacetOrderFlag();
  }

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

  /**
   * Collapses the facet, hiding 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();
  }

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

  /**
   *
   * @param value The string to find a caption for.
   * Returns the caption for a value or the value itself if no caption is available.
   */
  public getCaption(value: string) {
    const valueCaptions = this.options.valueCaption;
    const caption = valueCaptions[value];
    return caption ? caption : value;
  }

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

  private get analyticsFacetValue() {
    return this.values.selectedPath.join(this.options.delimitingCharacter);
  }

  public get analyticsFacetState(): IAnalyticsFacetState[] {
    if (!this.values.hasSelectedValue) {
      return [];
    }

    return [
      {
        field: this.options.field.toString(),
        id: this.options.id,
        title: this.options.title,
        facetType: this.facetType,
        facetPosition: this.position,
        value: this.analyticsFacetValue,
        displayValue: this.analyticsFacetValue,
        state: FacetValueState.selected,
        valuePosition: 1
      }
    ];
  }

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

  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(),
      expand: () => this.expand(),
      collapse: () => this.collapse()
    });
    this.element.appendChild(this.header.element);
  }

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

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

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

  private pathIsValidForSelection(path: any): path is string[] {
    return !Utils.isNullOrUndefined(path) && isArray(path);
  }

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

    const queryStatePath = data.attributes[this.queryStateAttribute];

    if (!this.pathIsValidForSelection(queryStatePath)) {
      return;
    }

    if (Utils.arrayEqual(queryStatePath, this.values.selectedPath)) {
      return;
    }

    queryStatePath.length ? this.selectPath(queryStatePath) : this.reset();
  }

  private initQueryStateEvents() {
    this.queryStateModel.registerNewAttribute(this.queryStateAttribute, []);
    this.bind.onQueryState<IAttributesChangedEventArg>(MODEL_EVENTS.CHANGE, undefined, data => this.handleQueryStateChanged(data));
  }

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

    this.dependsOnManager = new DependsOnManager(facetInfo);
  }

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

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

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

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

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

Initialization.registerAutoCreateComponent(DynamicHierarchicalFacet);
DynamicHierarchicalFacet.doExport();
