import {
  Component,
  OnInit,
  ChangeDetectionStrategy,
  OnDestroy,
  ViewChild,
  Input,
  ViewChildren,
  QueryList,
  ElementRef,
  AfterViewInit,
  ChangeDetectorRef,
} from '@angular/core';
import { Observable, of, Subject, BehaviorSubject, combineLatest, merge } from 'rxjs';
import { RemissionProduct, RemissionService, RemissionProcess } from '@isa/remission';
import { Store, Select } from '@ngxs/store';
import { RemissionSelectors } from 'apps/sales/src/app/core/store/selectors/remission.selectors';
import { map, switchMap, filter, catchError, takeUntil, take, distinctUntilChanged, debounceTime, tap, skip } from 'rxjs/operators';
import { RemissionListComponent } from '../../../components/remission-list';
import { RemissionResourceType } from '../../../models/remission-resource-type.model';
import { isNullOrUndefined } from 'util';
import { RemissionProcessStatuses } from 'apps/sales/src/app/core/models/remission-process-statuses.model';
import { RemissionToTopToBottomActionsComponent } from '../../../components/remission-to-top-to-bottom-actions';
import { UpdateShippingDocuent, SetRemissionActiveView } from 'apps/sales/src/app/core/store/actions/remission.actions';
import { RemissionActiveView } from '../../../models';
import { RemissionHelperService } from '../../../services';

@Component({
  selector: 'app-remission-product-list',
  templateUrl: './product-list.component.html',
  styleUrls: ['./product-list.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class RemissionProductListComponent implements OnInit, OnDestroy, AfterViewInit {
  @ViewChild('toTopToBottomActions')
  toTopToBottomActions: RemissionToTopToBottomActionsComponent;
  @ViewChild('remissionList')
  remissionList: RemissionListComponent;
  @ViewChildren('remissionListHitsRef') remissionHitsRef: QueryList<ElementRef>;

  @Input() remissionListContainer: HTMLElement;
  @Input() shippingDocumentContainer: HTMLElement;
  @Input() containerScrolled$: Observable<Event> = new Subject<Event>().asObservable();
  @Input() shippingDocumentRef: QueryList<ElementRef>;
  @Input() set slideOutOfView(shouldBeOutOfView: boolean) {
    if (!shouldBeOutOfView) {
      this._removeFromDocumentFlow = false;
    } else {
      setTimeout(() => {
        this._removeFromDocumentFlow = true;
        this.cdr.detectChanges();
      }, 400);
    }

    this._slideOutOfView = shouldBeOutOfView;
  }
  _slideOutOfView: boolean;
  _removeFromDocumentFlow = false;

  @Select(RemissionSelectors.getIsLoading) isLoading$: Observable<boolean>;
  @Select(RemissionSelectors.getRemissionProcess) remissionProcess$: Observable<RemissionProcess>;
  @Select(RemissionSelectors.getRemissionSource) source$: Observable<RemissionResourceType>;
  @Select(RemissionSelectors.getRemissionProcessStatuses)
  remissionProcessStatuses$: Observable<RemissionProcessStatuses>;
  @Select(RemissionSelectors.getRemissiontarget)
  remissionTarget$: Observable<string>;
  @Select(RemissionSelectors.getRemissionSource)
  remissionSource$: Observable<RemissionResourceType>;
  @Select(RemissionSelectors.getActiveView)
  activeView$: Observable<RemissionActiveView>;

  refetchRemissionProductsOnScrollUp$ = new Subject<void>();
  destroy$ = new Subject<void>();
  showJumpToTop$ = new BehaviorSubject<boolean>(false);
  showJumpToBottom$ = new BehaviorSubject<boolean>(false);

  intersectionObserverSubs = new Map<'top' | 'bottom', Function>();
  intersectionObserver$ = new Subject<{
    entry: IntersectionObserverEntry;
    observer: IntersectionObserver;
    type: 'top' | 'bottom';
  }>();
  isShippingDocumentVisible$ = new BehaviorSubject<boolean>(false);
  isNumberOfHitsVisible$ = new BehaviorSubject<boolean>(true);

  // TODO Refactor to be reactive
  viewShippingDocumentFromJumpToBottom = false;
  lastScrollTop = 0;

  fetchLastProducts$ = new Subject<void>();
  remissionProductsData$: Observable<{
    skip?: number;
    take?: number;
    hits?: number;
    items: RemissionProduct[];
    completed: boolean;
  }>;
  latestRemissionProductsData$ = new BehaviorSubject<{
    skip?: number;
    take?: number;
    hits?: number;
    items: RemissionProduct[];
    completed: boolean;
  }>(null);

  // Feature Flag: Indicates whether to load entire remission list on navigate to bottom
  loadAllProductsOnNavigateToBottom = false;

  constructor(
    private store: Store,
    private remissionService: RemissionService,
    private remissionHelperService: RemissionHelperService,
    private cdr: ChangeDetectorRef
  ) {}

  ngOnInit() {
    this.initProducts();
    this.initScrollHandler();
    this.initScrollRestoration();
  }

  ngAfterViewInit() {
    if (this.shippingDocumentRef && this.remissionHitsRef) {
      this.initIntersectionObservers();
    }
  }

  ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }

  initScrollRestoration() {
    this.activeView$.pipe(takeUntil(this.destroy$)).subscribe((activeView) => {
      if (activeView === 'Warenbegleitschein') {
        this.resetRemissionListScrollPosition(true);
      }
    });
  }

  initProducts() {
    this.remissionProductsData$ = this.remissionProcess$.pipe(
      filter((process) => !isNullOrUndefined(process) && !isNullOrUndefined(process.id)),
      take(1),
      map((process) => process.id),
      switchMap((remissionProcessId) =>
        this.remissionService.getRemissionProducts({ remissionProcessId }).pipe(
          filter((data) => !isNullOrUndefined(data)),
          map((data) => {
            if (!data.hits) {
              data.hits = (this.latestRemissionProductsData$.value && this.latestRemissionProductsData$.value.hits) || 0;
            }

            return data;
          }),
          catchError((err) => {
            console.error(err);
            return of({
              items: [],
              completed: true,
              hits: 0,
            });
          })
        )
      ),
      tap((data) => this.latestRemissionProductsData$.next(data))
    );

    merge(this.remissionSource$, this.remissionTarget$)
      .pipe(takeUntil(this.destroy$), skip(1))
      .subscribe((_) => this.latestRemissionProductsData$.next(null));
  }

  initScrollHandler() {
    this.containerScrolled$
      .pipe(
        takeUntil(this.destroy$),
        filter((scrollEvent) => !isNullOrUndefined(scrollEvent)),
        map((scrollEvent) => scrollEvent.target)
      )
      .subscribe((target) => this.scrollHandler(target as HTMLElement));
  }

  updateShippingDocument() {
    this.remissionProcess$
      .pipe(
        filter((process) => !isNullOrUndefined(process)),
        map((process) => process.id),
        take(1)
      )
      .subscribe((remissionProcessId) => {
        this.remissionHelperService.requestRefetchProducts();
        this.store.dispatch(new UpdateShippingDocuent(remissionProcessId));
      });
  }

  navigateToTop() {
    this.remissionListContainer.scrollTo({
      top: 0,
      behavior: 'smooth',
    });
  }

  navigateToWarenbegleitschein() {
    this.store.dispatch(new SetRemissionActiveView(RemissionActiveView.WBS));
  }

  scrollHandler(target: HTMLElement) {
    const { clientHeight, scrollHeight, scrollTop } = target;

    if (this.refetchProductsOnScroll()) {
      const shippingContainerHeight = this.shippingDocumentContainer.clientHeight;
      if (
        this.shouldRefreshProducts({
          clientHeight,
          scrollHeight,
          scrollTop,
          shippingContainerHeight,
        })
      ) {
        this.enableProductFetch();
      }
    }
  }

  private refetchProductsOnScroll(): boolean {
    return (
      !this.loadAllProductsOnNavigateToBottom &&
      this.remissionList &&
      !this.remissionList.isFullLoad &&
      this.shippingDocumentContainer &&
      !!this.shippingDocumentContainer.clientHeight
    );
  }

  private shouldRefreshProducts({
    clientHeight,
    scrollHeight,
    scrollTop,
    shippingContainerHeight,
  }: {
    clientHeight: number;
    scrollHeight: number;
    scrollTop: number;
    shippingContainerHeight: number;
  }): boolean {
    const pixelsScrolledUpToTriggerRefresh = 150;

    return (
      scrollTop + clientHeight - scrollHeight < pixelsScrolledUpToTriggerRefresh - shippingContainerHeight &&
      !!this.viewShippingDocumentFromJumpToBottom
    );
  }

  private enableProductFetch() {
    this.remissionList.isFullLoad = false;
    this.viewShippingDocumentFromJumpToBottom = false;
    this.refetchRemissionProductsOnScrollUp$.next();
  }

  private setScrollToTopBotomVisibility(topValue: boolean, bottomValue: boolean) {
    this.showJumpToTop$.next(topValue);
    this.showJumpToBottom$.next(bottomValue);
  }

  private initIntersectionObservers() {
    this.setUpIntersectionObservers();
    this.subscribeToIntersectionObservers();
  }

  private setUpIntersectionObservers() {
    this.remissionHitsRef.changes.pipe(this.returnFirstElement).subscribe((element) => {
      this.subscribeToIntersectionSub({
        element,
        sub: this.isNumberOfHitsVisible$,
        type: 'top',
      });
    });

    this.shippingDocumentRef.changes.pipe(this.returnFirstElement).subscribe((element) => {
      this.subscribeToIntersectionSub({
        element,
        sub: this.isShippingDocumentVisible$,
        type: 'bottom',
      });
    });
  }

  private returnFirstElement(el: Observable<any>): Observable<any> {
    return el.pipe(
      filter((elements) => !!elements.first),
      map((elements) => elements.first.nativeElement)
    );
  }

  private subscribeToIntersectionSub(params: { element: Element; sub: BehaviorSubject<boolean>; type: 'top' | 'bottom' }) {
    const { element, sub, type } = params;

    const unsubscribe = this.intersectionObserverSubs.get(type);
    if (unsubscribe) {
      unsubscribe();
    }

    this.intersectionObserverSubs.set(type, this.initIntersectionObserver(element, type));

    this.intersectionObserver$
      .pipe(
        filter((value) => value.type === type),
        takeUntil(this.destroy$)
      )
      .subscribe(async ({ entry }) => {
        const isEntryVisible = await this.isVisible(entry.target);

        if (isEntryVisible) {
          sub.next(true);
        } else {
          sub.next(false);
        }
      });
  }

  private initIntersectionObserver(element: Element, type: 'top' | 'bottom') {
    const intersectionObserver = new IntersectionObserver((entries, observer) => {
      entries.forEach((entry) => {
        this.intersectionObserver$.next({ entry, observer, type });
      });
    });

    intersectionObserver.observe(element);

    return () => {
      intersectionObserver.unobserve(element);
      intersectionObserver.disconnect();
    };
  }

  private async isVisible(element: Element) {
    return new Promise((resolve) => {
      const observer = new IntersectionObserver(([entry]) => {
        resolve(this.isIntersecting(entry));
        observer.disconnect();
      });
      observer.observe(element);
    });
  }

  private isIntersecting(entry: IntersectionObserverEntry): boolean {
    return entry.isIntersecting || entry.intersectionRatio > 0;
  }

  subscribeToIntersectionObservers() {
    combineLatest(
      this.isNumberOfHitsVisible$,
      this.isShippingDocumentVisible$,
      this.remissionProductsData$.pipe(
        map((data) => data.hits),
        distinctUntilChanged()
      ),
      this.isLoading$
    )
      .pipe(
        map(([isNumberOfHitsVisible, isShippingDocumentVisible, hits, isLoading]) => {
          if (!hits || hits === -1 || !!isLoading) {
            return {
              showTop: false,
              showBottom: false,
            };
          }

          return {
            showTop: !isShippingDocumentVisible,
            showBottom: !isNumberOfHitsVisible,
          };
        }),
        takeUntil(this.destroy$),
        debounceTime(350)
      )
      .subscribe(({ showTop, showBottom }) => this.setScrollToTopBotomVisibility(showTop, showBottom));
  }

  private resetRemissionListScrollPosition(waitForAnimationToComplete: boolean = false) {
    if (!this.remissionHitsRef || !this.remissionHitsRef.first) {
      return;
    }

    const container = this.remissionHitsRef.first.nativeElement;

    if (waitForAnimationToComplete) {
      setTimeout(() => {
        container.scrollTop = 0;
      }, 300);
    }
  }
}
