import { Component } from '../Base/Component';
import { l } from '../../strings/Strings';
import { ComponentOptions } from '../Base/ComponentOptions';
import { IFieldOption } from '../Base/IComponentOptions';
import { IComponentBindings } from '../Base/ComponentBindings';
import { $$, Dom } from '../../utils/Dom';
import { Initialization } from '../Base/Initialization';
import { exportGlobally } from '../../GlobalExports';
import { CategoryFacetTemplates } from './CategoryFacetTemplates';
import { CategoryValueRoot } from './CategoryValueRoot';
import { CategoryFacetQueryController } from '../../controllers/CategoryFacetQueryController';
import { SVGDom } from '../../utils/SVGDom';
import { SVGIcons } from '../../utils/SVGIcons';
import { QueryStateModel } from '../../models/QueryStateModel';
import 'styling/_CategoryFacet';
import { IAttributesChangedEventArg, MODEL_EVENTS } from '../../models/Model';
import { Utils } from '../../utils/Utils';
import { CategoryValue, CategoryValueParent } from './CategoryValue';
import { pluck, reduce, find, first, last, contains, isArray, keys } from 'underscore';
import { Assert } from '../../misc/Assert';
import { QueryEvents, IBuildingQueryEventArgs, IQuerySuccessEventArgs } from '../../events/QueryEvents';
import { CategoryFacetSearch } from './CategoryFacetSearch';
import { ICategoryFacetResult } from '../../rest/CategoryFacetResult';
import { BreadcrumbEvents, IPopulateBreadcrumbEventArgs } from '../../events/BreadcrumbEvents';
import { CategoryFacetBreadcrumb } from './CategoryFacetBreadcrumb';
import { ICategoryFacetValue } from '../../rest/CategoryFacetValue';
import { ISearchEndpoint } from '../../rest/SearchEndpointInterface';
import {
  IAnalyticsCategoryFacetMeta,
  analyticsActionCauseList,
  IAnalyticsActionCause,
  IAnalyticsFacetMeta
} from '../Analytics/AnalyticsActionListMeta';
import { CategoryFacetDebug } from './CategoryFacetDebug';
import { QueryBuilder } from '../Base/QueryBuilder';
import { IAutoLayoutAdjustableInsideFacetColumn } from '../SearchInterface/FacetColumnAutoLayoutAdjustment';
import { ResponsiveFacets } from '../ResponsiveComponents/ResponsiveFacets';
import { IResponsiveComponentOptions } from '../ResponsiveComponents/ResponsiveComponentsManager';
import { ResponsiveFacetOptions } from '../ResponsiveComponents/ResponsiveFacetOptions';
import { CategoryFacetHeader } from './CategoryFacetHeader';
import { AccessibleButton } from '../../utils/AccessibleButton';
import { IStringMap } from '../../rest/GenericParam';
import {
  DependsOnManager,
  IDependentFacet,
  IDependsOnCompatibleFacetOptions,
  IDependentFacetCondition
} from '../../utils/DependsOnManager';
import { ResultListUtils } from '../../utils/ResultListUtils';
import { CategoryFacetValuesTree } from './CategoryFacetValuesTree';

export interface ICategoryFacetOptions extends IResponsiveComponentOptions, IDependsOnCompatibleFacetOptions {
  field: IFieldOption;
  title?: string;
  numberOfResultsInFacetSearch?: number;
  id?: string;
  enableFacetSearch?: boolean;
  facetSearchDelay?: number;
  numberOfValues?: number;
  injectionDepth?: number;
  enableMoreLess?: boolean;
  pageSize?: number;
  delimitingCharacter?: string;
  debug?: boolean;
  basePath?: string[];
  maximumDepth?: number;
  valueCaption?: IStringMap<string>;
  dependsOn?: string;
  displaySearchOnTop?: boolean;
  displaySearchButton?: boolean;
}

export type CategoryValueDescriptor = {
  value: string;
  count: number;
  path: string[];
};

/**
 * The `CategoryFacet` component is a facet that renders values in a hierarchical fashion. It determines the filter to apply depending on the
 * current selected path of values.
 *
 * The path is a sequence of values that leads to a specific value in the hierarchy.
 * It is an array listing all the parents of a file (e.g., `['c', 'folder1']` for the `c:\folder1\text1.txt` file).
 *
 * This facet requires a [`field`]{@link CategoryFacet.options.field} with a special format to work correctly (see [Using the Category Facet Component](https://docs.coveo.com/en/2667)).
 *
 * @notSupportedIn salesforcefree
 * @availablesince [January 2019 Release (v2.5395.12)](https://docs.coveo.com/en/3277/#january-2019-release-v2539512)
 */
export class CategoryFacet extends Component implements IAutoLayoutAdjustableInsideFacetColumn {
  static doExport = () => {
    exportGlobally({
      CategoryFacet
    });
  };

  static ID = 'CategoryFacet';

  /**
   * The options for the component
   * @componentOptions
   */
  static options: ICategoryFacetOptions = {
    /**
     * 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 CategoryFacet.options.delimitingCharacter} option.
     *
     * To help you verify whether your fields are setup correctly, see the {@link CategoryFacet.options.debug} option
     * and the {@link CategoryFacet.debugValue} method.
     *
     * See [Using the Category Facet Component](https://docs.coveo.com/en/2667).
     */
    field: ComponentOptions.buildFieldOption({ required: true }),
    /**
     * The title to display at the top of the facet.
     *
     * Default value is the localized string for `NoTitle`.
     */
    title: ComponentOptions.buildLocalizedStringOption({
      localizedString: () => l('NoTitle')
    }),
    /**
     * 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 CategoryFacet.options.enableMoreLess} option.
     */
    numberOfValues: ComponentOptions.buildNumberOption({ defaultValue: 5, min: 0, section: 'CommonOptions' }),
    /**
     * Whether to display a search box at the bottom of the facet for searching among the available facet
     * [`field`]{@link CategoryFacet.options.field} values.
     *
     * See also the [`facetSearchDelay`]{@link CategoryFacet.options.facetSearchDelay}, and
     * [`numberOfResultsInFacetSearch`]{@link CategoryFacet.options.numberOfResultsInFacetSearch} options.
     *
     *
     * Default value is `true`.
     */
    enableFacetSearch: ComponentOptions.buildBooleanOption({ defaultValue: true }),
    /**
     * 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 CategoryFacet.options.field} option value.
     */
    id: ComponentOptions.buildStringOption({
      postProcessing: (value, options: ICategoryFacetOptions) => value || (options.field as string)
    }),
    /**
     * The *injection depth* to use.
     *
     * The injection depth determines how many results to scan in the index to ensure that the category facet lists all potential
     * facet values. Increasing this value enhances the accuracy of the listed values at the cost of performance.
     *
     * Default value is `1000`. Minimum value is `1000`.
     * @notSupportedIn salesforcefree
     *
     * @examples 1500
     */
    injectionDepth: ComponentOptions.buildNumberOption({ defaultValue: 1000, min: 1000 }),
    /**
     * If the [`enableFacetSearch`]{@link CategoryFacet.options.enableFacetSearch} option is `true`, specifies the number of
     * values to display in the facet search results popup.
     *
     * Default value is `15`. Minimum value is `1`.
     */
    numberOfResultsInFacetSearch: ComponentOptions.buildNumberOption({ defaultValue: 15, min: 1 }),
    /**
     * If the [`enableFacetSearch`]{@link CategoryFacet.options.enableFacetSearch} option is `true`, specifies the delay (in
     * milliseconds) before sending a search request to the server when the user starts typing in the category facet search box.
     *
     * Specifying a smaller value makes results appear faster. However, chances of having to cancel many requests
     * sent to the server increase as the user keeps on typing new characters.
     *
     * Default value is `100`. Minimum value is `0`.
     */
    facetSearchDelay: ComponentOptions.buildNumberOption({ defaultValue: 100, min: 0 }),
    /**
     * Whether to enable the **More** and **Less** buttons in the Facet.
     *
     * See also the [`pageSize`]{@link CategoryFacet.options.pageSize} option.
     *
     * Default value is `true`.
     */
    enableMoreLess: ComponentOptions.buildBooleanOption({ defaultValue: true }),
    /**
     * If the [`enableMoreLess`]{@link CategoryFacet.options.enableMoreLess} option is `true`, specifies the number of
     * additional results to fetch when clicking the **More** button.
     *
     * Default value is `10`. Minimum value is `1`.
     */
    pageSize: ComponentOptions.buildNumberOption({ defaultValue: 10, min: 1, depend: 'enableMoreLess' }),
    /**
     * The character that specifies the hierarchical dependency.
     *
     * **Example:**
     *
     * If your field has the following values:
     *
     * `@field: c; c>folder2; c>folder2>folder3;`
     *
     * The delimiting character is `>`.
     *
     * Default value is `|`.
     */
    delimitingCharacter: ComponentOptions.buildStringOption({ defaultValue: '|' }),
    /**
     * The path to use as the path prefix for every query.
     *
     * **Example:**
     *
     * You have the following files indexed on a file system:
     * ```
     * c:\
     *    folder1\
     *      text1.txt
     *    folder2\
     *      folder3\
     *        text2.txt
     * ```
     * Setting the `basePath` to `c` would display `folder1` and `folder2` in the `CategoryFacet`, but omit `c`.
     *
     * This options accepts an array of values. To specify a "deeper" starting path in your tree, you need to use comma-separated values.
     *
     * For example, setting `data-base-path="c,folder1"` on the component markup would display `folder3` in the `CategoryFacet`, but omit `c` and `folder1`.
     *
     */
    basePath: ComponentOptions.buildListOption<string>({ defaultValue: [] }),
    /**
     * The maximum number of levels to traverse in the hierarchy.
     * This option does not count the length of the base path. The depth depends on what is shown in the interface.
     *
     * Default value is `Number.MAX_VALUE`.
     */
    maximumDepth: ComponentOptions.buildNumberOption({ min: 1, defaultValue: Number.MAX_VALUE }),
    /**
     * Whether to activate field format debugging.
     * This options logs messages in the console for any potential encountered issues.
     * This option can have negative effects on performance, and should only be activated when debugging.
     */
    debug: ComponentOptions.buildBooleanOption({ defaultValue: false }),
    /**
     * Specifies a JSON object describing a mapping of facet values to their desired captions. See
     * [Normalizing Facet Value Captions](https://developers.coveo.com/x/jBsvAg).
     *
     * **Note:**
     * If this option is specified, the facet search box will be unavailable.
     *
     * **Examples:**
     *
     * You can set the option in the ['init']{@link init} call:
     * ```javascript
     * var myValueCaptions = {
     *   "txt" : "Text files",
     *   "html" : "Web page",
     *   [ ... ]
     * };
     *
     * Coveo.init(document.querySelector("#search"), {
     *   Facet : {
     *     valueCaption : myValueCaptions
     *   }
     * });
     * ```
     *
     * Or before the `init` call, using the ['options']{@link options} top-level function:
     * ```javascript
     * Coveo.options(document.querySelector("#search"), {
     *   Facet : {
     *     valueCaption : myValueCaptions
     *   }
     * });
     * ```
     *
     * Or directly in the markup:
     * ```html
     * <!-- Ensure that the double quotes are properly handled in data-value-caption. -->
     * <div class='CoveoCategoryFacet' data-field='@myotherfield' data-value-caption='{"txt":"Text files","html":"Web page"}'></div>
     * ```
     */
    valueCaption: ComponentOptions.buildJsonOption<IStringMap<string>>({ defaultValue: {} }),
    /**
     * The [id](@link Facet.options.id) of another facet in which at least one value must be selected in order
     * for the dependent category facet to be visible.
     *
     * **Default:** `undefined` and the category facet does not depend on any other facet to be displayed.
     *
     * @availablesince [September 2019 Release (v2.7023)](https://docs.coveo.com/en/2990/)
     */
    dependsOn: ComponentOptions.buildStringOption(),

    /**
     * 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 display the facet search widget above the facet values instead of below them.
     *
     * @availablesince [July 2020 Release (v2.9373)](https://docs.coveo.com/3293/)
     */
    displaySearchOnTop: ComponentOptions.buildBooleanOption({ defaultValue: false }),
    /**
     * Whether to display the facet search widget as a button instead of a search bar.
     *
     * @availablesince [July 2020 Release (v2.9373)](https://docs.coveo.com/3293/)
     */
    displaySearchButton: ComponentOptions.buildBooleanOption({ defaultValue: true }),
    ...ResponsiveFacetOptions
  };

  public categoryFacetQueryController: CategoryFacetQueryController;
  public listenToQueryStateChange = true;
  public categoryFacetSearch: CategoryFacetSearch;
  public activeCategoryValue: CategoryValue | undefined;
  public positionInQuery: number;
  public dependsOnManager: DependsOnManager;
  public isFieldValueCompatible = false;
  public static MAXIMUM_NUMBER_OF_VALUES_BEFORE_TRUNCATING = 15;
  public static NUMBER_OF_VALUES_TO_KEEP_AFTER_TRUNCATING = 10;

  private categoryValueRoot: CategoryValueRoot;
  private categoryFacetTemplates: CategoryFacetTemplates;
  private headerElement: HTMLElement;
  private currentPage: number;
  private moreLessContainer: Dom;
  private moreValuesToFetch: boolean = true;
  private showingWaitAnimation = false;
  private numberOfChildValuesCurrentlyDisplayed = 0;
  private numberOfValues: number;
  private categoryFacetValuesTree: CategoryFacetValuesTree;

  public static WAIT_ELEMENT_CLASS = 'coveo-category-facet-header-wait-animation';

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

    this.categoryFacetQueryController = new CategoryFacetQueryController(this);
    this.categoryFacetTemplates = new CategoryFacetTemplates();
    this.categoryValueRoot = new CategoryValueRoot($$(this.element), this.categoryFacetTemplates, this);
    this.categoryValueRoot.path = this.activePath;
    this.currentPage = 0;
    this.numberOfValues = this.options.numberOfValues;
    this.categoryFacetValuesTree = new CategoryFacetValuesTree();

    this.tryToInitFacetSearch();

    if (this.options.debug) {
      new CategoryFacetDebug(this);
    }

    ResponsiveFacets.init(this.root, this, this.options);
    this.initDependsOnManager();
    this.bind.onRootElement<IBuildingQueryEventArgs>(QueryEvents.buildingQuery, args => this.handleBuildingQuery(args));
    this.bind.onRootElement<IQuerySuccessEventArgs>(QueryEvents.querySuccess, args => this.handleQuerySuccess(args));
    this.bind.onRootElement(QueryEvents.duringQuery, () => this.addFading());
    this.bind.onRootElement(QueryEvents.deferredQuerySuccess, () => this.handleDeferredQuerySuccess());
    this.bind.onRootElement<IPopulateBreadcrumbEventArgs>(BreadcrumbEvents.populateBreadcrumb, args => this.handlePopulateBreadCrumb(args));
    this.bind.onRootElement(BreadcrumbEvents.clearBreadcrumb, () => this.handleClearBreadcrumb());
    this.buildFacetHeader();
    this.initQueryStateEvents();
  }

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

  public get activePath() {
    return this.queryStateModel.get(this.queryStateAttribute) || this.options.basePath;
  }

  public set activePath(newPath: string[]) {
    this.listenToQueryStateChange = false;
    this.queryStateModel.set(this.queryStateAttribute, newPath);
    this.listenToQueryStateChange = true;
  }

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

  public handleBuildingQuery(args: IBuildingQueryEventArgs) {
    this.positionInQuery = this.categoryFacetQueryController.putCategoryFacetInQueryBuilder(
      args.queryBuilder,
      this.activePath,
      this.numberOfValues + 1
    );
  }

  public scrollToTop() {
    ResultListUtils.scrollToTop(this.root);
  }

  private tryToInitFacetSearch() {
    if (!this.isFacetSearchAvailable) {
      return this.logDisabledFacetSearchWarning();
    }

    this.categoryFacetSearch = new CategoryFacetSearch(this, this.options.displaySearchButton);
  }

  private logDisabledFacetSearchWarning() {
    if (this.isEnableFacetSearchFalsy) {
      return;
    }

    const valueCaptionAttributeName = this.getOptionAttributeName('valueCaption');
    const enableFacetSearchAttributeName = this.getOptionAttributeName('enableFacetSearch');
    const field = this.options.field;

    this.logger.warn(`The search box is disabled on the ${field} CategoryFacet. To hide this warning,
    either remove the ${valueCaptionAttributeName} option or set the ${enableFacetSearchAttributeName} option to "false".`);
  }

  private getOptionAttributeName(optionName: keyof ICategoryFacetOptions) {
    return ComponentOptions.attrNameFromName(optionName);
  }

  private get isFacetSearchAvailable() {
    if (this.areValueCaptionsSpecified) {
      return false;
    }

    if (this.isEnableFacetSearchFalsy) {
      return false;
    }

    return true;
  }

  private get areValueCaptionsSpecified() {
    const valueCaptions = this.options.valueCaption;
    return keys(valueCaptions).length !== 0;
  }

  private get isEnableFacetSearchFalsy() {
    return !this.options.enableFacetSearch;
  }

  private get isCategoryEmpty() {
    return !this.categoryValueRoot.children.length;
  }

  private updateAppearance() {
    if (this.disabled || this.isCategoryEmpty) {
      return this.hide();
    }

    this.show();
  }

  public handleQuerySuccess(args: IQuerySuccessEventArgs) {
    if (Utils.isNullOrUndefined(args.results.categoryFacets)) {
      return this.notImplementedError();
    }

    if (Utils.isNullOrUndefined(args.results.categoryFacets[this.positionInQuery])) {
      return;
    }

    const numberOfRequestedValues = args.query.categoryFacets[this.positionInQuery].maximumNumberOfValues;
    const categoryFacetResult = args.results.categoryFacets[this.positionInQuery];
    this.moreValuesToFetch = numberOfRequestedValues == categoryFacetResult.values.length;
    this.clear();

    if (categoryFacetResult.notImplemented) {
      return this.notImplementedError();
    }

    if (!categoryFacetResult.values.length && !categoryFacetResult.parentValues.length) {
      return;
    }

    this.renderValues(categoryFacetResult, numberOfRequestedValues);
    if (this.isFacetSearchAvailable) {
      const facetSearch = this.categoryFacetSearch.build();
      this.options.displaySearchOnTop
        ? $$(facetSearch).insertBefore(this.categoryValueRoot.listRoot.el)
        : $$(facetSearch).insertAfter(this.categoryValueRoot.listRoot.el);
    }

    this.moreLessContainer = $$('div', { className: 'coveo-category-facet-more-less-container' });
    $$(this.element).append(this.moreLessContainer.el);

    if (this.options.enableMoreLess) {
      this.renderMoreLess();
    }

    if (!this.isPristine()) {
      $$(this.element).addClass('coveo-category-facet-non-empty-path');
    }
  }

  /**
   * Changes the active path.
   *
   */
  public changeActivePath(path: string[]) {
    this.activePath = path;
  }

  public async executeQuery() {
    this.showWaitingAnimation();
    try {
      await this.queryController.executeQuery();
    } finally {
      this.hideWaitingAnimation();
    }
  }

  /**
   * Reloads the facet with the same path.
   */
  public reload() {
    this.changeActivePath(this.activePath);
    this.logAnalyticsEvent(analyticsActionCauseList.categoryFacetReload);
    this.executeQuery();
  }

  /**
   * Returns all the visible parent values.
   * @returns simple object with three fields: `value`, `count` and `path`.
   */
  public getVisibleParentValues(): CategoryValueDescriptor[] {
    return this.getVisibleParentCategoryValues().map(categoryValue => categoryValue.getDescriptor());
  }

  private getVisibleParentCategoryValues() {
    if (this.categoryValueRoot.children.length == 0 || this.categoryValueRoot.children[0].children.length == 0) {
      return [];
    }
    let currentParentvalue = this.categoryValueRoot.children[0];
    const parentValues = [currentParentvalue];
    while (currentParentvalue.children.length != 0 && !Utils.arrayEqual(currentParentvalue.path, this.activePath)) {
      currentParentvalue = currentParentvalue.children[0];
      parentValues.push(currentParentvalue);
    }
    return parentValues;
  }

  /**
   * Shows more values according to {@link CategoryFacet.options.pageSize}.
   *
   * See the [`enableMoreLess`]{@link CategoryFacet.options.enableMoreLess}, and
   * [`numberOfValues`]{@link CategoryFacet.options.numberOfValues} options.
   */
  public showMore() {
    if (this.moreValuesToFetch) {
      this.currentPage++;
      this.numberOfValues = this.options.numberOfValues + this.currentPage * this.options.pageSize;
      this.reload();
      this.logAnalyticsFacetShowMoreLess(analyticsActionCauseList.facetShowMore);
    }
  }

  /**
   * Shows less values, up to the original number of values.
   *
   * See the [`enableMoreLess`]{@link CategoryFacet.options.enableMoreLess}, and
   * [`numberOfValues`]{@link CategoryFacet.options.numberOfValues} options.
   */
  public showLess() {
    if (this.currentPage > 0) {
      this.currentPage--;
      this.numberOfValues = this.options.numberOfValues + this.currentPage * this.options.pageSize;
      this.reload();
      this.logAnalyticsFacetShowMoreLess(analyticsActionCauseList.facetShowLess);
    }
  }

  /**
   * Returns the values at the bottom of the hierarchy. These are the values that are not yet applied to the query.
   * @returns simple object with three fields: `value`, `count` and `path`.
   */
  public getAvailableValues() {
    if (!this.activeCategoryValue) {
      return [];
    }
    return this.activeCategoryValue.children.map(categoryValue => {
      return {
        value: categoryValue.categoryValueDescriptor.value,
        count: categoryValue.categoryValueDescriptor.count,
        path: categoryValue.path
      };
    });
  }

  /**
   * Selects a value from the currently available values.
   * If the given value to select is not in the available values, it will throw an error.
   */
  public selectValue(value: string) {
    Assert.check(
      contains(pluck(this.getAvailableValues(), 'value'), value),
      'Failed while trying to select a value that is not available.'
    );
    const newPath = this.activePath.slice(0);
    newPath.push(value);
    this.changeActivePath(newPath);
    this.logAnalyticsEvent(analyticsActionCauseList.categoryFacetSelect);
    this.executeQuery();
  }

  /**
   * Deselects the last value in the hierarchy that is applied to the query. When at the top of the hierarchy, this method does nothing.
   */
  public deselectCurrentValue() {
    if (this.activePath.length == 0) {
      return;
    }

    const newPath = this.activePath.slice(0);
    newPath.pop();
    this.changeActivePath(newPath);
    this.logAnalyticsEvent(analyticsActionCauseList.categoryFacetSelect);
    this.executeQuery();
  }

  private resetPath() {
    this.changeActivePath(this.options.basePath);
  }

  /**
   * Resets the facet to its initial state.
   */
  public reset() {
    this.resetPath();
    this.logAnalyticsEvent(analyticsActionCauseList.categoryFacetClear);
    this.executeQuery();
  }

  /**
   * Hides the component.
   */
  public hide() {
    $$(this.element).addClass('coveo-hidden');
  }

  /**
   * Shows the component.
   */
  public show() {
    $$(this.element).removeClass('coveo-hidden');
  }

  public enable() {
    super.enable();
    this.updateAppearance();
  }

  public disable() {
    super.disable();
    this.updateAppearance();
  }

  /**
   * Goes through any value that contains the value parameter, and verifies whether there are missing parents.
   * Issues are then logged in the console.
   * If you do not want to specify a value, you can simply enable {@link CategoryFacet.options.debug} and do an empty query.
   */
  public async debugValue(value: string) {
    const queryBuilder = new QueryBuilder();
    this.categoryFacetQueryController.addDebugGroupBy(queryBuilder, value);
    const queryResults = await this.queryController.getEndpoint().search(queryBuilder.build());
    CategoryFacetDebug.analyzeResults(queryResults.groupByResults[0], this.options.delimitingCharacter);
  }

  /**
   *
   * @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 showWaitingAnimation() {
    this.ensureDom();
    if (!this.showingWaitAnimation) {
      $$(this.headerElement).find('.coveo-category-facet-header-wait-animation').style.visibility = 'visible';
      this.showingWaitAnimation = true;
    }
  }

  public hideWaitingAnimation() {
    this.ensureDom();
    if (this.showingWaitAnimation) {
      $$(this.headerElement).find('.coveo-category-facet-header-wait-animation').style.visibility = 'hidden';
      this.showingWaitAnimation = false;
    }
  }

  public logAnalyticsEvent(eventName: IAnalyticsActionCause, path = this.activePath) {
    this.usageAnalytics.logSearchEvent<IAnalyticsCategoryFacetMeta>(eventName, {
      categoryFacetId: this.options.id,
      categoryFacetField: this.options.field.toString(),
      categoryFacetPath: path,
      categoryFacetTitle: this.options.title
    });
  }

  public getEndpoint(): ISearchEndpoint {
    return this.queryController.getEndpoint();
  }

  get children(): CategoryValue[] {
    return this.categoryValueRoot.children;
  }

  private renderValues(categoryFacetResult: ICategoryFacetResult, numberOfRequestedValues: number) {
    this.categoryFacetValuesTree.storeNewValues(categoryFacetResult);
    let sortedParentValues = this.sortParentValues(categoryFacetResult.parentValues);
    let currentParentValue: CategoryValueParent = this.categoryValueRoot;
    let needToTruncate = false;
    let pathOfLastTruncatedParentValue: string[];

    const numberOfItemsInFirstSlice = Math.floor(CategoryFacet.NUMBER_OF_VALUES_TO_KEEP_AFTER_TRUNCATING / 2);
    const numberOfItemsInSecondSlice = Math.ceil(CategoryFacet.NUMBER_OF_VALUES_TO_KEEP_AFTER_TRUNCATING / 2);

    sortedParentValues = this.hideBasePathInParentValues(sortedParentValues);

    if (this.shouldTruncate(sortedParentValues)) {
      pathOfLastTruncatedParentValue = this.findPathOfLastTruncatedParentValue(sortedParentValues, numberOfItemsInSecondSlice);
      needToTruncate = true;
      sortedParentValues = first(sortedParentValues, numberOfItemsInFirstSlice).concat(
        last(sortedParentValues, numberOfItemsInSecondSlice)
      );
    }

    if (!this.isPristine()) {
      this.addAllCategoriesButton();
    }

    for (let i = 0; i < sortedParentValues.length; i++) {
      currentParentValue = currentParentValue.renderAsParent(sortedParentValues[i]);

      // We do not want to make the "last" parent selectable, as clicking it would be a noop (re-selecting the same filter)
      const isLastParent = i == sortedParentValues.length - 1;
      if (!isLastParent) {
        (currentParentValue as CategoryValue).makeSelectable().showCollapseArrow();
      }

      if (needToTruncate) {
        if (i == numberOfItemsInFirstSlice - 1) {
          this.addEllipsis();
        }

        if (i == numberOfItemsInFirstSlice) {
          currentParentValue.path = [...pathOfLastTruncatedParentValue, sortedParentValues[i].value];
        }
      }
    }

    const childrenValuesToRender = this.moreValuesToFetch
      ? categoryFacetResult.values.slice(0, numberOfRequestedValues - 1)
      : categoryFacetResult.values.slice(0, numberOfRequestedValues);

    this.numberOfChildValuesCurrentlyDisplayed = childrenValuesToRender.length;

    currentParentValue.renderChildren(childrenValuesToRender);
    this.activeCategoryValue = currentParentValue as CategoryValue;
  }

  private hideBasePathInParentValues(parentValues: ICategoryFacetValue[]) {
    if (Utils.arrayEqual(first(this.activePath, this.options.basePath.length), this.options.basePath)) {
      parentValues = last(parentValues, parentValues.length - this.options.basePath.length);
    }
    return parentValues;
  }

  private shouldTruncate(parentValues: ICategoryFacetValue[]) {
    return parentValues.length > CategoryFacet.MAXIMUM_NUMBER_OF_VALUES_BEFORE_TRUNCATING;
  }

  private addEllipsis() {
    this.categoryValueRoot.listRoot.append(this.categoryFacetTemplates.buildEllipsis().el);
  }

  private findPathOfLastTruncatedParentValue(sortedParentValues: ICategoryFacetValue[], numberOfItemsInSecondSlice: number) {
    const indexOfLastTruncatedParentValue = sortedParentValues.length - numberOfItemsInSecondSlice - 1;
    return reduce(first(sortedParentValues, indexOfLastTruncatedParentValue + 1), (path, parentValue) => [...path, parentValue.value], []);
  }

  private addAllCategoriesButton() {
    const allCategories = this.categoryFacetTemplates.buildAllCategoriesButton();
    new AccessibleButton()
      .withLabel(l('AllCategories'))
      .withElement(allCategories)
      .withSelectAction(() => {
        this.reset();
        this.scrollToTop();
      })
      .build();
    this.categoryValueRoot.listRoot.append(allCategories.el);
  }

  private isPristine() {
    return Utils.arrayEqual(this.activePath, this.options.basePath);
  }

  private buildFacetHeader() {
    const facetHeader = new CategoryFacetHeader({ categoryFacet: this, title: this.options.title });
    this.headerElement = facetHeader.build();
    $$(this.element).prepend(this.headerElement);
  }

  private handleQueryStateChanged(data: IAttributesChangedEventArg) {
    if (this.listenToQueryStateChange) {
      let path = data.attributes[this.queryStateAttribute];
      if (!Utils.isNullOrUndefined(path) && isArray(path) && path.length != 0) {
        this.activePath = path;
      }
    }
  }

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

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

  private dependsOnReset() {
    this.changeActivePath(this.options.basePath);
    this.clear();
  }

  private addFading() {
    $$(this.element).addClass('coveo-category-facet-values-fade');
  }

  private handleDeferredQuerySuccess() {
    this.updateAppearance();
    this.removeFading();
  }

  private removeFading() {
    $$(this.element).removeClass('coveo-category-facet-values-fade');
  }

  private notImplementedError() {
    const errorMessage = 'Category Facets are not supported by your current search endpoint. Disabling this component.';
    this.logger.error(errorMessage);
    this.disable();
  }

  private sortParentValues(parentValues: ICategoryFacetValue[]) {
    if (this.activePath.length != parentValues.length) {
      this.logger.warn('Inconsistent CategoryFacet results: Number of parent values results does not equal length of active path');
      return parentValues;
    }

    const sortedParentvalues: ICategoryFacetValue[] = [];
    for (const pathElement of this.activePath) {
      const currentParentValue = find(parentValues, parentValue => parentValue.value.toLowerCase() == pathElement.toLowerCase());
      if (!currentParentValue) {
        this.logger.warn('Inconsistent CategoryFacet results: path not consistent with parent values results');
        return parentValues;
      }
      sortedParentvalues.push(currentParentValue);
    }
    return sortedParentvalues;
  }

  private renderMoreLess() {
    if (this.numberOfChildValuesCurrentlyDisplayed > this.options.numberOfValues) {
      this.moreLessContainer.append(this.buildLessButton());
    }

    if (this.moreValuesToFetch) {
      this.moreLessContainer.append(this.buildMoreButton());
    }
  }

  private clear() {
    this.categoryValueRoot.clear();
    if (this.isFacetSearchAvailable) {
      this.categoryFacetSearch.clear();
    }
    this.moreLessContainer && this.moreLessContainer.detach();
    $$(this.element).removeClass('coveo-category-facet-non-empty-path');
  }

  private buildMoreButton() {
    const svgContainer = $$('span', { className: 'coveo-facet-more-icon' }, SVGIcons.icons.arrowDown).el;
    SVGDom.addClassToSVGInContainer(svgContainer, 'coveo-facet-more-icon-svg');
    const more = $$('div', { className: 'coveo-category-facet-more' }, svgContainer);

    new AccessibleButton()
      .withElement(more)
      .withSelectAction(() => this.showMore())
      .withLabel(l('ShowMoreFacetResults', this.options.title))
      .build();

    return more.el;
  }

  private buildLessButton() {
    const svgContainer = $$('span', { className: 'coveo-facet-less-icon' }, SVGIcons.icons.arrowUp).el;
    SVGDom.addClassToSVGInContainer(svgContainer, 'coveo-facet-less-icon-svg');
    const less = $$('div', { className: 'coveo-category-facet-less' }, svgContainer);

    new AccessibleButton()
      .withElement(less)
      .withSelectAction(() => this.showLess())
      .withLabel(l('ShowLessFacetResults', this.options.title))
      .build();

    return less.el;
  }

  private handlePopulateBreadCrumb(args: IPopulateBreadcrumbEventArgs) {
    if (this.isPristine()) {
      return;
    }

    const lastParentValue = this.categoryFacetValuesTree.getValueForLastPartInPath(this.activePath);
    const descriptor: CategoryValueDescriptor = {
      path: this.activePath,
      count: lastParentValue.numberOfResults,
      value: lastParentValue.value
    };

    const resetFacet = () => {
      this.logAnalyticsEvent(analyticsActionCauseList.breadcrumbFacet);
      this.reset();
    };

    const categoryFacetBreadcrumbBuilder = new CategoryFacetBreadcrumb(this, resetFacet, descriptor, { headingLevel: args.headingLevel });

    args.breadcrumbs.push({ element: categoryFacetBreadcrumbBuilder.build() });
  }

  private handleClearBreadcrumb() {
    this.changeActivePath(this.options.basePath);
  }

  private logAnalyticsFacetShowMoreLess(cause: IAnalyticsActionCause) {
    this.usageAnalytics.logCustomEvent<IAnalyticsFacetMeta>(
      cause,
      {
        facetId: this.options.id,
        facetField: this.options.field.toString(),
        facetTitle: this.options.title
      },
      this.element
    );
  }
}

Initialization.registerAutoCreateComponent(CategoryFacet);
CategoryFacet.doExport();
