import 'styling/_Pager';
import {
  IBuildingQueryEventArgs,
  INewQueryEventArgs,
  INoResultsEventArgs,
  IQuerySuccessEventArgs,
  QueryEvents
} from '../../events/QueryEvents';
import { ResultListEvents } from '../../events/ResultListEvents';
import { exportGlobally } from '../../GlobalExports';
import { Assert } from '../../misc/Assert';
import { IAttributeChangedEventArg, MODEL_EVENTS } from '../../models/Model';
import { QueryStateModel, QUERY_STATE_ATTRIBUTES } from '../../models/QueryStateModel';
import { l } from '../../strings/Strings';
import { AccessibleButton } from '../../utils/AccessibleButton';
import { DeviceUtils } from '../../utils/DeviceUtils';
import { $$ } from '../../utils/Dom';
import { ResultListUtils } from '../../utils/ResultListUtils';
import { SVGDom } from '../../utils/SVGDom';
import { SVGIcons } from '../../utils/SVGIcons';
import { analyticsActionCauseList, IAnalyticsActionCause, IAnalyticsPagerMeta } from '../Analytics/AnalyticsActionListMeta';
import { Component } from '../Base/Component';
import { IComponentBindings } from '../Base/ComponentBindings';
import { ComponentOptions } from '../Base/ComponentOptions';
import { Initialization } from '../Base/Initialization';

export interface IPagerOptions {
  numberOfPages: number;
  enableNavigationButton: boolean;
  maxNumberOfPages: number;
  maximumNumberOfResultsFromIndex: number;
}

/**
 * The Pager component attaches itself to a `div` element and renders widgets that allow the end user to navigate
 * through the different result pages.
 *
 * This component takes care of triggering a query with the correct result range whenever the end user selects a page or
 * uses the navigation buttons (**Previous** and **Next**).
 */
export class Pager extends Component {
  static ID = 'Pager';

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

  /**
   * The options for the Pager
   * @componentOptions
   */
  static options: IPagerOptions = {
    /**
     * Specifies how many page links to display in the pager.
     *
     * Default value is `5` on a desktop computers and `3` on a mobile device. Minimum value is `1`.
     */
    numberOfPages: ComponentOptions.buildNumberOption({
      defaultFunction: () => {
        if (DeviceUtils.isMobileDevice()) {
          return 3;
        } else {
          return 5;
        }
      },
      min: 1
    }),

    /**
     * Specifies whether the **Previous** and **Next** buttons should appear at each end of the pager when appropriate.
     *
     * The default value is `true`.
     */
    enableNavigationButton: ComponentOptions.buildBooleanOption({ defaultValue: true }),

    /**
     * Specifies the maximum number of pages to display if enough results are available.
     *
     * This property is typically set when the default number of accessible results from the index has been changed from its default value of `1000` (10 results per page X 100 `maxNumberOfPages`).
     * Default value is `100`
     *
     * @deprecated This is a deprecated option. The `Pager` now automatically adapts itself on each new query, so you no longer need to specify a value for this option. However, if the default maximum number of accessible results value was changed on your Coveo index, you should use the [`maximumNumberOfResultsFromIndex`]{@link Pager.options.maximumNumberOfResultsFromIndex} option to specify the new value.
     */
    maxNumberOfPages: ComponentOptions.buildNumberOption({
      defaultValue: undefined,
      deprecated:
        'This is a deprecated option. The pager will automatically adapt itself on each new query. You no longer need to specify this option. Use maximumNumberOfResultsFromIndex instead.'
    }),
    /**
     * Specifies the maximum number of results that the index can return for any query.
     *
     * Default value is `1000` in a Coveo index.
     *
     * If this value was modified in your Coveo index, you must specify the new value in this option for the Pager component to work properly
     */
    maximumNumberOfResultsFromIndex: ComponentOptions.buildNumberOption({
      defaultValue: 1000
    })
  };

  private listenToQueryStateChange = true;
  private ignoreNextQuerySuccess = false;

  private _currentPage: number;

  // The normal behavior of this component is to reset to page 1 when a new
  // query is performed by other components (i.e. not pagers).
  //
  // This behavior is overridden when the 'first' state is
  // programmatically modified.
  private needToReset = true;

  private list: HTMLElement;

  /**
   * Creates a new Pager. Binds multiple query events ({@link QueryEvents.newQuery}, {@link QueryEvents.buildingQuery},
   * {@link QueryEvents.querySuccess}, {@link QueryEvents.queryError} and {@link QueryEvents.noResults}. Renders itself
   * on every query success.
   * @param element The HTMLElement on which to instantiate the component (normally a `div`).
   * @param options The options for the Pager component.
   * @param bindings The bindings that the component requires to function normally. If not set, these will be
   * automatically resolved (with a slower execution time).
   */
  constructor(public element: HTMLElement, public options?: IPagerOptions, bindings?: IComponentBindings) {
    super(element, Pager.ID, bindings);
    this.options = ComponentOptions.initComponentOptions(element, Pager, options);
    this.currentPage = 1;

    this.bind.onRootElement(QueryEvents.newQuery, (args: INewQueryEventArgs) => this.handleNewQuery(args));
    this.bind.onRootElement(QueryEvents.buildingQuery, (args: IBuildingQueryEventArgs) => this.handleBuildingQuery(args));
    this.bind.onRootElement(QueryEvents.querySuccess, (args: IQuerySuccessEventArgs) => this.handleQuerySuccess(args));
    this.bind.onRootElement(QueryEvents.queryError, () => this.handleQueryError());
    this.bind.onRootElement(QueryEvents.noResults, (args: INoResultsEventArgs) => this.handleNoResults(args));
    this.bind.onQueryState(MODEL_EVENTS.CHANGE_ONE, QUERY_STATE_ATTRIBUTES.FIRST, (data: IAttributeChangedEventArg) =>
      this.handleQueryStateFirstResultChanged(data)
    );
    this.bind.onQueryState(MODEL_EVENTS.CHANGE_ONE, QUERY_STATE_ATTRIBUTES.NUMBER_OF_RESULTS, (data: IAttributeChangedEventArg) =>
      this.handleQueryStateNumberOfResultsPerPageChanged(data)
    );
    this.addAlwaysActiveListeners();
    this.addAccessibilityAttributes();

    this.list = $$('ul', {
      className: 'coveo-pager-list'
    }).el;
    element.appendChild(this.list);
  }

  /**
   * The current page (1-based index).
   */
  public get currentPage(): number {
    return this._currentPage;
  }

  public set currentPage(value: number) {
    let sanitizedValue = value;

    if (isNaN(value)) {
      this.logger.warn(`Unable to set pager current page to an invalid value: ${value}. Resetting to 1.`);
      sanitizedValue = 1;
    }

    sanitizedValue = Math.max(Math.min(sanitizedValue, this.getMaxNumberOfPagesForCurrentResultsPerPage()), 1);
    sanitizedValue = Math.floor(sanitizedValue);

    this._currentPage = sanitizedValue;
  }

  /**
   * Sets the current page, then executes a query.
   *
   * Also logs an event in the usage analytics (`pageNumber` by default) with the new current page number as meta data.
   *
   * @param pageNumber The page number to navigate to.
   * @param analyticCause The event to log in the usage analytics.
   */
  public setPage(pageNumber: number, analyticCause: IAnalyticsActionCause = analyticsActionCauseList.pagerNumber) {
    Assert.exists(pageNumber);
    this.currentPage = pageNumber;
    this.updateQueryStateModel(this.getFirstResultNumber(this.currentPage));
    this.usageAnalytics.logCustomEvent<IAnalyticsPagerMeta>(analyticCause, { pagerNumber: this.currentPage }, this.element);
    this.queryController.executeQuery({
      ignoreWarningSearchEvent: true,
      keepLastSearchUid: true,
      origin: this
    });
  }

  /**
   * Navigates to the previous page, then executes a query.
   *
   * Also logs the `pagePrevious` event in the usage analytics with the new current page number as meta data.
   */
  public previousPage() {
    this.setPage(this.currentPage - 1, analyticsActionCauseList.pagerPrevious);
  }

  /**
   * Navigates to the next page, then executes a query.
   *
   * Also logs the `pageNext` event in the usage analytics with the new current page number as meta data.
   */
  public nextPage() {
    this.setPage(this.currentPage + 1, analyticsActionCauseList.pagerNext);
  }

  private addAccessibilityAttributes() {
    if (!this.element.getAttribute('role')) {
      this.element.setAttribute('role', 'navigation');
    }
    if (!this.element.hasAttribute('aria-label')) {
      this.element.setAttribute('aria-label', l('Pagination'));
    }
  }

  private addAlwaysActiveListeners() {
    this.searchInterface.element.addEventListener(ResultListEvents.newResultsDisplayed, () =>
      ResultListUtils.hideIfInfiniteScrollEnabled(this)
    );
  }

  private getMaxNumberOfPagesForCurrentResultsPerPage() {
    return Math.ceil(this.options.maximumNumberOfResultsFromIndex / this.searchInterface.resultsPerPage);
  }

  private handleNewQuery(data: INewQueryEventArgs) {
    const triggeredByPagerOrDebugMode = data && data.origin && (data.origin.type == Pager.ID || data.origin.type == 'Debug');
    if (this.needToReset && !triggeredByPagerOrDebugMode) {
      this.currentPage = 1;
      this.updateQueryStateModel(this.getFirstResultNumber(this.currentPage));
    }
    this.needToReset = true;
  }

  private updateQueryStateModel(attrValue: number) {
    this.listenToQueryStateChange = false;
    this.queryStateModel.set(QueryStateModel.attributesEnum.first, attrValue);
    this.listenToQueryStateChange = true;
  }

  private handleQueryError() {
    this.reset();
  }

  private handleQuerySuccess(data: IQuerySuccessEventArgs) {
    this.reset();
    if (this.ignoreNextQuerySuccess) {
      this.ignoreNextQuerySuccess = false;
    } else {
      Assert.isNotUndefined(data);

      const firstResult = data.query.firstResult;
      const count = data.results.totalCountFiltered;

      const pagerBoundary = this.computePagerBoundary(firstResult, count);
      this.currentPage = pagerBoundary.currentPage;
      if (pagerBoundary.end - pagerBoundary.start > 0) {
        for (let i = pagerBoundary.start; i <= pagerBoundary.end; i++) {
          const page = i;
          const isCurrentPage = page === this.currentPage;
          const button = $$(
            'span',
            {
              className: 'coveo-pager-list-item-text coveo-pager-anchor',
              tabindex: 0,
              ariaPressed: `${isCurrentPage}`
            },
            i.toString(10)
          ).el;

          const listItem = $$('li', {
            className: 'coveo-pager-list-item'
          }).el;
          if (isCurrentPage) {
            $$(listItem).addClass('coveo-active');
            button.setAttribute('aria-current', 'page');
          }

          const clickAction = (e: Event) => {
            e.stopPropagation();
            this.handleClickPage(page);
          };

          new AccessibleButton()
            .withElement(button)
            .withLabel(l('PageNumber', i.toString(10)))
            .withClickAction(clickAction)
            .withEnterKeyboardAction(clickAction)
            .build();

          $$(listItem).on('click', e => clickAction(e));

          listItem.appendChild(button);
          this.list.appendChild(listItem);
        }

        if (this.options.enableNavigationButton && pagerBoundary.lastResultPage > 1) {
          this.renderNavigationButton(pagerBoundary);
        }
      }
    }
  }

  private handleNoResults(data: INoResultsEventArgs) {
    let lastValidPage;
    if (data.results.totalCount > 0) {
      // First scenario : The index returned less results than expected (because of folding).
      // Recalculate the last valid page, and change to that new page.
      const possibleValidPage = this.computePagerBoundary(data.results.totalCountFiltered, data.results.totalCount).lastResultPage;
      if (this.currentPage > possibleValidPage) {
        lastValidPage = possibleValidPage;
      }
    } else if (this.currentPage > this.getMaxNumberOfPagesForCurrentResultsPerPage()) {
      // Second scenario : Someone tried to access a non-valid page by the URL for example, which is  higher than the current possible with the number of
      // possible results. The last valid page will be the maximum number of results avaiable from the index.
      lastValidPage = this.getMaxNumberOfPagesForCurrentResultsPerPage();
    }

    // This needs to be deferred because we still want all the "querySuccess" callbacks the complete their execution
    // before triggering/queuing the next query;
    // if we cannot find a lastValidPage to go to, do nothing : this means it's a query that simply return nothing.
    if (lastValidPage != null) {
      this.currentPage = lastValidPage;
      data.retryTheQuery = true;
      this.needToReset = false;
      this.ignoreNextQuerySuccess = false;
      this.updateQueryStateModel(this.getFirstResultNumber(this.currentPage));
    }
  }

  private reset() {
    $$(this.list).empty();
  }

  private handleBuildingQuery(data: IBuildingQueryEventArgs) {
    Assert.exists(data);
    const eventArgs = this.getQueryEventArgs();
    data.queryBuilder.firstResult = eventArgs.first;

    // Set the number of results only if it was not already set by external code
    // Most of the time this will be set by either : the SearchInterface with the resultsPerPage option
    // Or by the ResultsPerPage component (so the end user decides).
    // Pager will realistically never set this value itself.
    if (data.queryBuilder.numberOfResults == null) {
      data.queryBuilder.numberOfResults = eventArgs.count;
    }

    const maxResultNumber = data.queryBuilder.firstResult + data.queryBuilder.numberOfResults;
    const numOfExcessResults = Math.max(0, maxResultNumber - this.options.maximumNumberOfResultsFromIndex);

    data.queryBuilder.numberOfResults -= numOfExcessResults;
  }

  private computePagerBoundary(firstResult: number, totalCount: number) {
    const resultPerPage = this.searchInterface.resultsPerPage;
    const currentPage = Math.floor(firstResult / resultPerPage) + 1;
    const lastPageNumber = Math.max(Math.min(Math.ceil(totalCount / resultPerPage), this.getMaxNumberOfPagesForCurrentResultsPerPage()), 1);

    const halfLength = Math.floor(this.options.numberOfPages / 2);
    const firstPageNumber = Math.max(Math.min(currentPage - halfLength, lastPageNumber - this.options.numberOfPages + 1), 1);
    const endPageNumber = Math.min(firstPageNumber + this.options.numberOfPages - 1, lastPageNumber);

    return {
      start: firstPageNumber,
      end: endPageNumber,
      lastResultPage: lastPageNumber,
      currentPage: currentPage
    };
  }

  private renderNavigationButton(pagerBoundary: { start: number; end: number; lastResultPage: number; currentPage: number }) {
    if (this.currentPage > 1) {
      const previous = this.renderPreviousButton();
      this.list.insertBefore(previous.el, this.list.firstChild);
    }

    if (this.currentPage < pagerBoundary.lastResultPage) {
      const next = this.renderNextButton();
      this.list.appendChild(next.el);
    }
  }

  private renderPreviousButton() {
    const previousListItem = $$('li', {
      className: 'coveo-pager-previous coveo-pager-anchor coveo-pager-list-item'
    });

    const previousButton = $$('span', {
      title: l('Previous'),
      tabindex: 0
    });

    const previousIcon = $$(
      'span',
      {
        className: 'coveo-pager-previous-icon'
      },
      SVGIcons.icons.pagerLeftArrow
    );

    SVGDom.addClassToSVGInContainer(previousIcon.el, 'coveo-pager-previous-icon-svg');

    previousButton.append(previousIcon.el);
    previousListItem.append(previousButton.el);

    new AccessibleButton()
      .withElement(previousButton)
      .withLabel(l('Previous'))
      .withSelectAction(() => this.handleClickPrevious())
      .build();

    return previousListItem;
  }

  private renderNextButton() {
    const nextListItem = $$('li', {
      className: 'coveo-pager-next coveo-pager-anchor coveo-pager-list-item'
    });

    const nextButton = $$('span', {
      title: l('Next'),
      tabindex: 0
    });

    const nextIcon = $$(
      'span',
      {
        className: 'coveo-pager-next-icon'
      },
      SVGIcons.icons.pagerRightArrow
    );

    SVGDom.addClassToSVGInContainer(nextIcon.el, 'coveo-pager-next-icon-svg');

    nextButton.append(nextIcon.el);
    nextListItem.append(nextButton.el);

    new AccessibleButton()
      .withElement(nextButton)
      .withLabel(l('Next'))
      .withSelectAction(() => this.handleClickNext())
      .build();

    return nextListItem;
  }

  private handleQueryStateFirstResultChanged(data: IAttributeChangedEventArg) {
    if (!this.listenToQueryStateChange) {
      return;
    }
    Assert.exists(data);
    this.needToReset = false;
    const firstResult = data.value;
    this.currentPage = this.fromFirstResultsToPageNumber(firstResult);
  }

  private handleQueryStateNumberOfResultsPerPageChanged(data: IAttributeChangedEventArg) {
    const firstResult = this.queryStateModel.get(QUERY_STATE_ATTRIBUTES.FIRST);
    this.searchInterface.resultsPerPage = data.value;
    this.currentPage = this.fromFirstResultsToPageNumber(firstResult);
  }

  private handleClickPage(pageNumber: number) {
    Assert.exists(pageNumber);
    this.setPage(pageNumber);
  }

  private handleClickPrevious() {
    this.previousPage();
  }

  private handleClickNext() {
    this.nextPage();
  }

  private fromFirstResultsToPageNumber(firstResult: number): number {
    return firstResult / this.searchInterface.resultsPerPage + 1;
  }

  private getFirstResultNumber(pageNumber: number = this.currentPage): number {
    return (pageNumber - 1) * this.searchInterface.resultsPerPage;
  }

  private getQueryEventArgs() {
    return {
      count: this.searchInterface.resultsPerPage,
      first: this.getFirstResultNumber()
    };
  }
}

Initialization.registerAutoCreateComponent(Pager);
