import {
  ChangeDetectionStrategy, ChangeDetectorRef,
  Component,
  EventEmitter,
  Input,
  OnDestroy,
  OnInit,
  Output,
  ViewEncapsulation
} from '@angular/core'
import { ActivatedRoute, Router } from '@angular/router';

import { Subscription, Observable } from 'rxjs';
import { isNumeric } from 'rxjs/internal-compatibility';
import { isEqual, isObject, transform } from 'lodash';

import { HostWindowService } from '../host-window.service';
import { HostWindowState } from '../search/host-window.reducer';
import { PaginationComponentOptions } from './pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { hasValue, isNotEmpty } from '../empty.util';
import { PageInfo } from '../../core/shared/page-info.model';
import { difference } from '../object.util';

/**
 * The default pagination controls component.
 */
@Component({
  exportAs: 'paginationComponent',
  selector: 'ds-pagination',
  styleUrls: ['pagination.component.scss'],
  templateUrl: 'pagination.component.html',
  changeDetection: ChangeDetectionStrategy.Default,
  encapsulation: ViewEncapsulation.Emulated
})
export class PaginationComponent implements OnDestroy, OnInit {
  /**
   * Number of items in collection.
   */
  @Input() collectionSize: number;

  /**
   * Page state of a Remote paginated objects.
   */
  @Input() pageInfoState: Observable<PageInfo> = undefined;

  /**
   * Configuration for the NgbPagination component.
   */
  @Input() paginationOptions: PaginationComponentOptions;

  /**
   * Sort configuration for this component.
   */
  @Input() sortOptions: SortOptions;

  /**
   * An event fired when the page is changed.
   * Event's payload equals to the newly selected page.
   */
  @Output() pageChange: EventEmitter<number> = new EventEmitter<number>();

  /**
   * An event fired when the page wsize is changed.
   * Event's payload equals to the newly selected page size.
   */
  @Output() pageSizeChange: EventEmitter<number> = new EventEmitter<number>();

  /**
   * An event fired when the sort direction is changed.
   * Event's payload equals to the newly selected sort direction.
   */
  @Output() sortDirectionChange: EventEmitter<SortDirection> = new EventEmitter<SortDirection>();

  /**
   * An event fired when the sort field is changed.
   * Event's payload equals to the newly selected sort field.
   */
  @Output() sortFieldChange: EventEmitter<string> = new EventEmitter<string>();

  /**
   * An event fired when the pagination is changed.
   * Event's payload equals to the newly selected sort field.
   */
  @Output() paginationChange: EventEmitter<any> = new EventEmitter<any>();

  /**
   * Option for hiding the pagination detail
   */
  @Input() public hidePaginationDetail = false;

  /**
   * Option for hiding the gear
   */
  @Input() public hideGear = false;

  /**
   * Option for hiding the pager when there is less than 2 pages
   */
  @Input() public hidePagerWhenSinglePage = true;

  /**
   * Option for disabling updating and reading route parameters on pagination changes
   * In other words, changing pagination won't add or update the url parameters on the current page, and the url
   * parameters won't affect the pagination of this component
   */
  @Input() public disableRouteParameterUpdate = false;

  /**
   * Current page.
   */
  public currentPage;

  /**
   * Current page in the state of a Remote paginated objects.
   */
  public currentPageState: number = undefined;

  /**
   * Current URL query parameters
   */
  public currentQueryParams: any;

  /**
   * An observable of HostWindowState type
   */
  public hostWindow: Observable<HostWindowState>;

  /**
   * ID for the pagination instance. Only useful if you wish to
   * have more than once instance at a time in a given component.
   */
  private id: string;

  /**
   * A boolean that indicate if is an extra small devices viewport.
   */
  public isXs: boolean;

  /**
   * Number of items per page.
   */
  public pageSize;

  /**
   * Declare SortDirection enumeration to use it in the template
   */
  public sortDirections = SortDirection;

  /**
   * A number array that represents options for a context pagination limit.
   */
  public pageSizeOptions: number[];

  /**
   * Direction in which to sort: ascending or descending
   */
  public sortDirection: SortDirection = SortDirection.ASC;

  /**
   * Name of the field that's used to sort by
   */
  public sortField = 'id';

  /**
   * Array to track all subscriptions and unsubscribe them onDestroy
   * @type {Array}
   */
  private subs: Subscription[] = [];

  /**
   * Method provided by Angular. Invoked after the constructor.
   */
  ngOnInit() {
    this.subs.push(this.hostWindowService.isXs()
      .subscribe((status: boolean) => {
        this.isXs = status;
        this.cdRef.markForCheck();
      }));
    this.checkConfig(this.paginationOptions);
    this.initializeConfig();
    // Listen to changes
    if (!this.disableRouteParameterUpdate) {
      this.subs.push(this.route.queryParams
        .subscribe((queryParams) => {
          this.initializeParams(queryParams);
        }));
    }
  }

  /**
   * Initialize the route and current parameters
   * This method will fix any invalid or missing parameters
   * @param params
   */
  private initializeParams(params) {
    if (this.isEmptyPaginationParams(params)) {
      this.initializeConfig(params);
    } else {
      this.currentQueryParams = params;
      const fixedProperties = this.validateParams(params);
      if (isNotEmpty(fixedProperties)) {
        if (!this.disableRouteParameterUpdate) {
          this.fixRoute(fixedProperties);
        } else {
          this.initializeParams(fixedProperties);
        }
      } else {
        this.setFields();
      }
    }
  }

  private fixRoute(fixedProperties) {
    this.updateRoute(fixedProperties);
  }

  /**
   * Method provided by Angular. Invoked when the instance is destroyed.
   */
  ngOnDestroy() {
    this.subs
      .filter((sub) => hasValue(sub))
      .forEach((sub) => sub.unsubscribe());
  }

  /**
   * Initializes all default variables
   */
  private initializeConfig(queryParams: any = {}) {
    // Set initial values
    this.id = this.paginationOptions.id || null;
    this.pageSizeOptions = this.paginationOptions.pageSizeOptions;
    this.currentPage = this.paginationOptions.currentPage;
    this.pageSize = this.paginationOptions.pageSize;
    if (this.sortOptions) {
      this.sortDirection = this.sortOptions.direction;
      this.sortField = this.sortOptions.field;
    }
    this.currentQueryParams = Object.assign({}, queryParams, {
      pageId: this.id,
      page: this.currentPage,
      pageSize: this.pageSize,
      sortDirection: this.sortDirection,
      sortField: this.sortField
    });
  }

  /**
   * @param cdRef
   *    ChangeDetectorRef is a singleton service provided by Angular.
   * @param route
   *    Route is a singleton service provided by Angular.
   * @param router
   *    Router is a singleton service provided by Angular.
   * @param hostWindowService
   *    the HostWindowService singleton.
   */
  constructor(private cdRef: ChangeDetectorRef,
              private route: ActivatedRoute,
              private router: Router,
              public hostWindowService: HostWindowService) {
  }

  /**
   * Method to change the route to the given page
   *
   * @param page
   *    The page being navigated to.
   */
  public doPageChange(page: number) {
    this.updateParams(Object.assign({}, this.currentQueryParams, { pageId: this.id, page: page.toString() }));
  }

  /**
   * Method to change the route to the given page size
   *
   * @param pageSize
   *    The page size being navigated to.
   */
  public doPageSizeChange(pageSize: number) {
    this.updateParams(Object.assign({}, this.currentQueryParams,{ pageId: this.id, page: 1, pageSize: pageSize }));
  }

  /**
   * Method to change the route to the given sort direction
   *
   * @param sortDirection
   *    The sort direction being navigated to.
   */
  public doSortDirectionChange(sortDirection: SortDirection) {
    this.updateParams(Object.assign({}, this.currentQueryParams,{ pageId: this.id, page: 1, sortDirection: sortDirection }));
  }

  /**
   * Method to change the route to the given sort field
   *
   * @param sortField
   *    The sort field being navigated to.
   */
  public doSortFieldChange(field: string) {
    this.updateParams(Object.assign(this.currentQueryParams,{ pageId: this.id, page: 1, sortField: field }));
  }

  /**
   * Method to set the current page and trigger page change events
   *
   * @param page
   *    The new page value
   */
  public setPage(page: number) {
    this.currentPage = page;
    this.pageChange.emit(page);
    this.emitPaginationChange();
  }

  /**
   * Method to set the current page size and trigger page size change events
   *
   * @param pageSize
   *    The new page size value.
   */
  public setPageSize(pageSize: number) {
    this.pageSize = pageSize;
    this.pageSizeChange.emit(pageSize);
    this.emitPaginationChange();
  }

  /**
   * Method to set the current sort direction and trigger sort direction change events
   *
   * @param sortDirection
   *    The new sort directionvalue.
   */
  public setSortDirection(sortDirection: SortDirection) {
    this.sortDirection = sortDirection;
    this.sortDirectionChange.emit(sortDirection);
    this.emitPaginationChange();
  }

  /**
   * Method to set the current sort field and trigger sort field change events
   *
   * @param sortField
   *    The new sort field.
   */
  public setSortField(field: string) {
    this.sortField = field;
    this.sortFieldChange.emit(field);
    this.emitPaginationChange();
  }

  /**
   * Method to emit a general pagination change event
   */
  private emitPaginationChange() {
    this.paginationChange.emit(
      {
        pagination: Object.assign(
          new PaginationComponentOptions(),
          {
            id: this.id,
            currentPage: this.currentPage,
            pageSize: this.pageSize,
          }),
        sort: Object.assign(
          new SortOptions(this.sortField, this.sortDirection)
        )
      })
  }

  /**
   * Update the current query params and optionally update the route
   * @param params
   */
  private updateParams(params: {}) {
    if (isNotEmpty(difference(params, this.currentQueryParams))) {
      if (!this.disableRouteParameterUpdate) {
        this.updateRoute(params);
      } else {
        this.initializeParams(params);
      }
    }
  }

  /**
   * Method to update the route parameters
   */
  private updateRoute(params: {}) {
    if (isNotEmpty(difference(params, this.currentQueryParams))) {
      this.router.navigate([], {
        queryParams: Object.assign({}, this.currentQueryParams, params),
        queryParamsHandling: 'merge'
      });
    }
  }

  private difference(object, base) {
    const changes = (o, b) => {
      return transform(o, (result, value, key) => {
        if (!isEqual(value, b[key]) && isNotEmpty(value)) {
          result[key] = (isObject(value) && isObject(b[key])) ? changes(value, b[key]) : value;
        }
      });
    };
    return changes(object, base);
  }

  /**
   * Method to get pagination details of the current viewed page.
   */
  public getShowingDetails(collectionSize: number): any {
    let showingDetails = { range: null + ' - ' + null, total: null };
    if (collectionSize) {
      let firstItem;
      let lastItem;
      const pageMax = this.pageSize * this.currentPage;

      firstItem = this.pageSize * (this.currentPage - 1) + 1;
      if (collectionSize > pageMax) {
        lastItem = pageMax;
      } else {
        lastItem = collectionSize;
      }
      showingDetails = { range: firstItem + ' - ' + lastItem, total: collectionSize };
    }
    return showingDetails;
  }

  /**
   * Method to validate query params
   *
   * @param page
   *    The page number to validate
   * @param pageSize
   *    The page size to validate
   * @returns valid parameters if initial parameters were invalid
   */
  private validateParams(params: any): any {
    const validPage = this.validatePage(params.page);
    const filteredSize = this.validatePageSize(params.pageSize);
    const fixedFields: any = {};
    if (+params.page !== validPage) {
      fixedFields.page = validPage.toString();
    }
    if (+params.pageSize !== filteredSize) {
      fixedFields.pageSize = filteredSize.toString();
    }
    return fixedFields;
  }

  /**
   * Method to update all pagination variables to the current query parameters
   */
  private setFields() {
    // set fields only when page id is the one configured for this pagination instance
    if (this.currentQueryParams.pageId === this.id) {
      // (+) converts string to a number
      const page = this.currentQueryParams.page;
      if (this.currentPage !== +page) {
        this.setPage(+page);
      }

      const pageSize = this.currentQueryParams.pageSize;
      if (this.pageSize !== +pageSize) {
        this.setPageSize(+pageSize);
      }

      const sortDirection = this.currentQueryParams.sortDirection;
      if (this.sortDirection !== sortDirection) {
        this.setSortDirection(sortDirection);
      }

      const sortField = this.currentQueryParams.sortField;
      if (this.sortField !== sortField) {
        this.setSortField(sortField);
      }
      this.cdRef.detectChanges();
    }
  }

  /**
   * Method to validate the current page value
   *
   * @param page
   *    The page number to validate
   * @returns returns valid page value
   */
  private validatePage(page: any): number {
    let result = this.currentPage;
    if (isNumeric(page)) {
      result = +page;
    }
    return result;
  }

  /**
   * Method to validate the current page size value
   *
   * @param page size
   *    The page size to validate
   * @returns returns valid page size value
   */
  private validatePageSize(pageSize: any): number {
    const filteredPageSize = this.pageSizeOptions.find((x) => x === +pageSize);
    let result = this.pageSize;
    if (filteredPageSize) {
      result = +pageSize;
    }
    return result;
  }

  /**
   * Method to ensure options passed contains the required properties.
   *
   * @param paginateOptions
   *    The paginate options object.
   */
  private checkConfig(paginateOptions: any) {
    const required = ['id', 'currentPage', 'pageSize', 'pageSizeOptions'];
    const missing = required.filter((prop) => {
      return !(prop in paginateOptions);
    });
    if (0 < missing.length) {
      throw new Error('Paginate: Argument is missing the following required properties: ' + missing.join(', '));
    }
  }

  /**
   * Method to check if none of the query params necessary for pagination are filled out.
   *
   * @param paginateOptions
   *    The paginate options object.
   */
  private isEmptyPaginationParams(paginateOptions): boolean {
    const properties = ['id', 'currentPage', 'pageSize', 'pageSizeOptions'];
    const missing = properties.filter((prop) => {
      return !(prop in paginateOptions);
    });

    return properties.length === missing.length;
  }

  /**
   * Property to check whether the current pagination object has multiple pages
   * @returns true if there are multiple pages, else returns false
   */
  get hasMultiplePages(): boolean {
    return this.collectionSize > this.pageSize;
  }

  /**
   * Property to check whether the current pagination should show a bottom pages
   * @returns true if a bottom pages should be shown, else returns false
   */
  get shouldShowBottomPager(): boolean {
    return this.hasMultiplePages || !this.hidePagerWhenSinglePage
  }
}
