'use strict';

import React, { Component } from 'react';
import PropTypes from 'prop-types';
import PageView from './PageView';
import BreakView from './BreakView';
import { classNameIfDefined } from './utils';

export default class PaginationBoxView extends Component {
  static propTypes = {
    pageCount: PropTypes.number.isRequired,
    pageRangeDisplayed: PropTypes.number,
    marginPagesDisplayed: PropTypes.number,
    previousLabel: PropTypes.node,
    previousAriaLabel: PropTypes.string,
    prevPageRel: PropTypes.string,
    prevRel: PropTypes.string,
    nextLabel: PropTypes.node,
    nextAriaLabel: PropTypes.string,
    nextPageRel: PropTypes.string,
    nextRel: PropTypes.string,
    breakLabel: PropTypes.oneOfType([PropTypes.string, PropTypes.node]),
    breakAriaLabels: PropTypes.shape({
      forward: PropTypes.string,
      backward: PropTypes.string,
    }),
    hrefBuilder: PropTypes.func,
    hrefAllControls: PropTypes.bool,
    onPageChange: PropTypes.func,
    onPageActive: PropTypes.func,
    onClick: PropTypes.func,
    initialPage: PropTypes.number,
    forcePage: PropTypes.number,
    disableInitialCallback: PropTypes.bool,
    containerClassName: PropTypes.string,
    className: PropTypes.string,
    pageClassName: PropTypes.string,
    pageLinkClassName: PropTypes.string,
    pageLabelBuilder: PropTypes.func,
    activeClassName: PropTypes.string,
    activeLinkClassName: PropTypes.string,
    previousClassName: PropTypes.string,
    nextClassName: PropTypes.string,
    previousLinkClassName: PropTypes.string,
    nextLinkClassName: PropTypes.string,
    disabledClassName: PropTypes.string,
    disabledLinkClassName: PropTypes.string,
    breakClassName: PropTypes.string,
    breakLinkClassName: PropTypes.string,
    extraAriaContext: PropTypes.string,
    ariaLabelBuilder: PropTypes.func,
    eventListener: PropTypes.string,
    renderOnZeroPageCount: PropTypes.func,
    selectedPageRel: PropTypes.string,
  };

  static defaultProps = {
    pageRangeDisplayed: 2,
    marginPagesDisplayed: 3,
    activeClassName: 'selected',
    previousLabel: 'Previous',
    previousClassName: 'previous',
    previousAriaLabel: 'Previous page',
    prevPageRel: 'prev',
    prevRel: 'prev',
    nextLabel: 'Next',
    nextClassName: 'next',
    nextAriaLabel: 'Next page',
    nextPageRel: 'next',
    nextRel: 'next',
    breakLabel: '...',
    breakAriaLabels: { forward: 'Jump forward', backward: 'Jump backward' },
    disabledClassName: 'disabled',
    disableInitialCallback: false,
    pageLabelBuilder: (page) => page,
    eventListener: 'onClick',
    renderOnZeroPageCount: undefined,
    selectedPageRel: 'canonical',
    hrefAllControls: false,
  };

  constructor(props) {
    super(props);

    if (props.initialPage !== undefined && props.forcePage !== undefined) {
      console.warn(
        `(react-paginate): Both initialPage (${props.initialPage}) and forcePage (${props.forcePage}) props are provided, which is discouraged.` +
          ' Use exclusively forcePage prop for a controlled component.\nSee https://reactjs.org/docs/forms.html#controlled-components'
      );
    }

    let initialSelected;
    if (props.initialPage) {
      initialSelected = props.initialPage;
    } else if (props.forcePage) {
      initialSelected = props.forcePage;
    } else {
      initialSelected = 0;
    }

    this.state = {
      selected: initialSelected,
    };
  }

  componentDidMount() {
    const {
      initialPage,
      disableInitialCallback,
      extraAriaContext,
      pageCount,
      forcePage,
    } = this.props;
    // Call the callback with the initialPage item:
    if (typeof initialPage !== 'undefined' && !disableInitialCallback) {
      this.callCallback(initialPage);
    }

    if (extraAriaContext) {
      console.warn(
        'DEPRECATED (react-paginate): The extraAriaContext prop is deprecated. You should now use the ariaLabelBuilder instead.'
      );
    }

    if (!Number.isInteger(pageCount)) {
      console.warn(
        `(react-paginate): The pageCount prop value provided is not an integer (${pageCount}). Did you forget a Math.ceil()?`
      );
    }

    if (initialPage !== undefined && initialPage > pageCount - 1) {
      console.warn(
        `(react-paginate): The initialPage prop provided is greater than the maximum page index from pageCount prop (${initialPage} > ${
          pageCount - 1
        }).`
      );
    }

    if (forcePage !== undefined && forcePage > pageCount - 1) {
      console.warn(
        `(react-paginate): The forcePage prop provided is greater than the maximum page index from pageCount prop (${forcePage} > ${
          pageCount - 1
        }).`
      );
    }
  }

  componentDidUpdate(prevProps) {
    if (
      this.props.forcePage !== undefined &&
      this.props.forcePage !== prevProps.forcePage
    ) {
      if (this.props.forcePage > this.props.pageCount - 1) {
        console.warn(
          `(react-paginate): The forcePage prop provided is greater than the maximum page index from pageCount prop (${
            this.props.forcePage
          } > ${this.props.pageCount - 1}).`
        );
      }

      this.setState({ selected: this.props.forcePage });
    }

    if (
      Number.isInteger(prevProps.pageCount) &&
      !Number.isInteger(this.props.pageCount)
    ) {
      console.warn(
        `(react-paginate): The pageCount prop value provided is not an integer (${this.props.pageCount}). Did you forget a Math.ceil()?`
      );
    }
  }

  handlePreviousPage = (event) => {
    const { selected } = this.state;

    this.handleClick(event, null, selected > 0 ? selected - 1 : undefined, {
      isPrevious: true,
    });
  };

  handleNextPage = (event) => {
    const { selected } = this.state;
    const { pageCount } = this.props;

    this.handleClick(
      event,
      null,
      selected < pageCount - 1 ? selected + 1 : undefined,
      { isNext: true }
    );
  };

  handlePageSelected = (selected, event) => {
    if (this.state.selected === selected) {
      this.callActiveCallback(selected);
      this.handleClick(event, null, undefined, { isActive: true });
      return;
    }

    this.handleClick(event, null, selected);
  };

  handlePageChange = (selected) => {
    if (this.state.selected === selected) {
      return;
    }
    this.setState({ selected });

    // Call the callback with the new selected item:
    this.callCallback(selected);
  };

  getEventListener = (handlerFunction) => {
    const { eventListener } = this.props;
    return {
      [eventListener]: handlerFunction,
    };
  };

  getForwardJump() {
    const { selected } = this.state;
    const { pageCount, pageRangeDisplayed } = this.props;

    const forwardJump = selected + pageRangeDisplayed;
    return forwardJump >= pageCount ? pageCount - 1 : forwardJump;
  }

  getBackwardJump() {
    const { selected } = this.state;
    const { pageRangeDisplayed } = this.props;

    const backwardJump = selected - pageRangeDisplayed;
    return backwardJump < 0 ? 0 : backwardJump;
  }

  handleClick = (
    event,
    index,
    nextSelectedPage,
    {
      isPrevious = false,
      isNext = false,
      isBreak = false,
      isActive = false,
    } = {}
  ) => {
    event.preventDefault ? event.preventDefault() : (event.returnValue = false);
    const { selected } = this.state;
    const { onClick } = this.props;

    let newPage = nextSelectedPage;

    if (onClick) {
      const onClickReturn = onClick({
        index,
        selected,
        nextSelectedPage,
        event,
        isPrevious,
        isNext,
        isBreak,
        isActive,
      });
      if (onClickReturn === false) {
        // We abord standard behavior and let parent handle
        // all behavior.
        return;
      }
      if (Number.isInteger(onClickReturn)) {
        // We assume parent want to go to the returned page.
        newPage = onClickReturn;
      }
    }

    if (newPage !== undefined) {
      this.handlePageChange(newPage);
    }
  };

  handleBreakClick = (index, event) => {
    const { selected } = this.state;

    this.handleClick(
      event,
      index,
      selected < index ? this.getForwardJump() : this.getBackwardJump(),
      { isBreak: true }
    );
  };

  getElementHref(pageIndex) {
    const { hrefBuilder, pageCount, hrefAllControls } = this.props;
    if (!hrefBuilder) return;
    if (hrefAllControls || (pageIndex >= 0 && pageIndex < pageCount)) {
      return hrefBuilder(pageIndex + 1, pageCount, this.state.selected);
    }
  }

  ariaLabelBuilder(pageIndex) {
    const selected = pageIndex === this.state.selected;
    if (
      this.props.ariaLabelBuilder &&
      pageIndex >= 0 &&
      pageIndex < this.props.pageCount
    ) {
      let label = this.props.ariaLabelBuilder(pageIndex + 1, selected);
      // DEPRECATED: The extraAriaContext prop was used to add additional context
      // to the aria-label. Users should now use the ariaLabelBuilder instead.
      if (this.props.extraAriaContext && !selected) {
        label = label + ' ' + this.props.extraAriaContext;
      }
      return label;
    }
  }

  callCallback = (selectedItem) => {
    if (
      this.props.onPageChange !== undefined &&
      typeof this.props.onPageChange === 'function'
    ) {
      this.props.onPageChange({ selected: selectedItem });
    }
  };

  callActiveCallback = (selectedItem) => {
    if (
      this.props.onPageActive !== undefined &&
      typeof this.props.onPageActive === 'function'
    ) {
      this.props.onPageActive({ selected: selectedItem });
    }
  };

  getElementPageRel = (index) => {
    const { selected } = this.state;
    const { nextPageRel, prevPageRel, selectedPageRel } = this.props;

    if (selected - 1 === index) {
      return prevPageRel;
    } else if (selected === index) {
      return selectedPageRel;
    } else if (selected + 1 === index) {
      return nextPageRel;
    }
    return undefined;
  };

  getPageElement(index) {
    const { selected } = this.state;
    const {
      pageClassName,
      pageLinkClassName,
      activeClassName,
      activeLinkClassName,
      extraAriaContext,
      pageLabelBuilder,
    } = this.props;

    return (
      <PageView
        key={index}
        pageSelectedHandler={this.handlePageSelected.bind(null, index)}
        selected={selected === index}
        rel={this.getElementPageRel(index)}
        pageClassName={pageClassName}
        pageLinkClassName={pageLinkClassName}
        activeClassName={activeClassName}
        activeLinkClassName={activeLinkClassName}
        extraAriaContext={extraAriaContext}
        href={this.getElementHref(index)}
        ariaLabel={this.ariaLabelBuilder(index)}
        page={index + 1}
        pageLabelBuilder={pageLabelBuilder}
        getEventListener={this.getEventListener}
      />
    );
  }

  pagination = () => {
    const items = [];
    const {
      pageRangeDisplayed,
      pageCount,
      marginPagesDisplayed,
      breakLabel,
      breakClassName,
      breakLinkClassName,
      breakAriaLabels,
    } = this.props;

    const { selected } = this.state;

    if (pageCount <= pageRangeDisplayed) {
      for (let index = 0; index < pageCount; index++) {
        items.push(this.getPageElement(index));
      }
    } else {
      let leftSide = pageRangeDisplayed / 2;
      let rightSide = pageRangeDisplayed - leftSide;

      // If the selected page index is on the default right side of the pagination,
      // we consider that the new right side is made up of it (= only one break element).
      // If the selected page index is on the default left side of the pagination,
      // we consider that the new left side is made up of it (= only one break element).
      if (selected > pageCount - pageRangeDisplayed / 2) {
        rightSide = pageCount - selected;
        leftSide = pageRangeDisplayed - rightSide;
      } else if (selected < pageRangeDisplayed / 2) {
        leftSide = selected;
        rightSide = pageRangeDisplayed - leftSide;
      }

      let createPageView = (index) => this.getPageElement(index);
      let index;
      let breakView;

      // First pass: process the pages or breaks to display (or not).
      const pagesBreaking = [];
      for (index = 0; index < pageCount; index++) {
        const page = index + 1;

        // If the page index is lower than the margin defined,
        // the page has to be displayed on the left side of
        // the pagination.
        if (page <= marginPagesDisplayed) {
          pagesBreaking.push({
            type: 'page',
            index,
            display: createPageView(index),
          });
          continue;
        }

        // If the page index is greater than the page count
        // minus the margin defined, the page has to be
        // displayed on the right side of the pagination.
        if (page > pageCount - marginPagesDisplayed) {
          pagesBreaking.push({
            type: 'page',
            index,
            display: createPageView(index),
          });
          continue;
        }

        // If it is the first element of the array the rightSide need to be adjusted,
        //  otherwise an extra element will be rendered
        const adjustedRightSide =
          selected === 0 && pageRangeDisplayed > 1 ? rightSide - 1 : rightSide;

        // If the page index is near the selected page index
        // and inside the defined range (pageRangeDisplayed)
        // we have to display it (it will create the center
        // part of the pagination).
        if (
          index >= selected - leftSide &&
          index <= selected + adjustedRightSide
        ) {
          pagesBreaking.push({
            type: 'page',
            index,
            display: createPageView(index),
          });
          continue;
        }

        // If the page index doesn't meet any of the conditions above,
        // we check if the last item of the current "items" array
        // is a break element. If not, we add a break element, else,
        // we do nothing (because we don't want to display the page).
        if (
          breakLabel &&
          pagesBreaking.length > 0 &&
          pagesBreaking[pagesBreaking.length - 1].display !== breakView &&
          // We do not show break if only one active page is displayed.
          (pageRangeDisplayed > 0 || marginPagesDisplayed > 0)
        ) {
          const useBreakAriaLabel =
            index < selected
              ? breakAriaLabels.backward
              : breakAriaLabels.forward;
          breakView = (
            <BreakView
              key={index}
              breakAriaLabel={useBreakAriaLabel}
              breakLabel={breakLabel}
              breakClassName={breakClassName}
              breakLinkClassName={breakLinkClassName}
              breakHandler={this.handleBreakClick.bind(null, index)}
              getEventListener={this.getEventListener}
            />
          );
          pagesBreaking.push({ type: 'break', index, display: breakView });
        }
      }
      // Second pass: we remove breaks containing one page to the actual page.
      pagesBreaking.forEach((pageElement, i) => {
        let actualPageElement = pageElement;
        // 1 2 3 4 5 6 7 ... 9 10
        //         |
        // 1 2 ... 4 5 6 7 8 9 10
        //             |
        // The break should be replaced by the page.
        if (
          pageElement.type === 'break' &&
          pagesBreaking[i - 1] &&
          pagesBreaking[i - 1].type === 'page' &&
          pagesBreaking[i + 1] &&
          pagesBreaking[i + 1].type === 'page' &&
          pagesBreaking[i + 1].index - pagesBreaking[i - 1].index <= 2
        ) {
          actualPageElement = {
            type: 'page',
            index: pageElement.index,
            display: createPageView(pageElement.index),
          };
        }
        // We add the displayed elements in the same pass, to avoid another iteration.
        items.push(actualPageElement.display);
      });
    }

    return items;
  };

  render() {
    const { renderOnZeroPageCount } = this.props;
    if (this.props.pageCount === 0 && renderOnZeroPageCount !== undefined) {
      return renderOnZeroPageCount
        ? renderOnZeroPageCount(this.props)
        : renderOnZeroPageCount;
    }
    const {
      disabledClassName,
      disabledLinkClassName,
      pageCount,
      className,
      containerClassName,
      previousLabel,
      previousClassName,
      previousLinkClassName,
      previousAriaLabel,
      prevRel,
      nextLabel,
      nextClassName,
      nextLinkClassName,
      nextAriaLabel,
      nextRel,
    } = this.props;

    const { selected } = this.state;

    const isPreviousDisabled = selected === 0;
    const isNextDisabled = selected === pageCount - 1;

    const previousClasses = `${classNameIfDefined(previousClassName)}${
      isPreviousDisabled ? ` ${classNameIfDefined(disabledClassName)}` : ''
    }`;
    const nextClasses = `${classNameIfDefined(nextClassName)}${
      isNextDisabled ? ` ${classNameIfDefined(disabledClassName)}` : ''
    }`;

    const previousLinkClasses = `${classNameIfDefined(previousLinkClassName)}${
      isPreviousDisabled ? ` ${classNameIfDefined(disabledLinkClassName)}` : ''
    }`;
    const nextLinkClasses = `${classNameIfDefined(nextLinkClassName)}${
      isNextDisabled ? ` ${classNameIfDefined(disabledLinkClassName)}` : ''
    }`;

    const previousAriaDisabled = isPreviousDisabled ? 'true' : 'false';
    const nextAriaDisabled = isNextDisabled ? 'true' : 'false';

    return (
      <ul
        className={className || containerClassName}
        role="navigation"
        aria-label="Pagination"
      >
        <li className={previousClasses}>
          <a
            className={previousLinkClasses}
            href={this.getElementHref(selected - 1)}
            tabIndex={isPreviousDisabled ? '-1' : '0'}
            role="button"
            onKeyPress={this.handlePreviousPage}
            aria-disabled={previousAriaDisabled}
            aria-label={previousAriaLabel}
            rel={prevRel}
            {...this.getEventListener(this.handlePreviousPage)}
          >
            {previousLabel}
          </a>
        </li>

        {this.pagination()}

        <li className={nextClasses}>
          <a
            className={nextLinkClasses}
            href={this.getElementHref(selected + 1)}
            tabIndex={isNextDisabled ? '-1' : '0'}
            role="button"
            onKeyPress={this.handleNextPage}
            aria-disabled={nextAriaDisabled}
            aria-label={nextAriaLabel}
            rel={nextRel}
            {...this.getEventListener(this.handleNextPage)}
          >
            {nextLabel}
          </a>
        </li>
      </ul>
    );
  }
}
