import {
  AsyncPipe,
  NgFor,
  NgIf,
} from '@angular/common';
import {
  ChangeDetectorRef,
  Component,
  ElementRef,
  EventEmitter,
  HostListener,
  Input,
  OnDestroy,
  OnInit,
  Output,
} from '@angular/core';
import {
  FormsModule,
  ReactiveFormsModule,
  UntypedFormControl,
} from '@angular/forms';
import { TranslateModule } from '@ngx-translate/core';
import { InfiniteScrollModule } from 'ngx-infinite-scroll';
import {
  BehaviorSubject,
  from as observableFrom,
  Observable,
  of as observableOf,
  Subscription,
} from 'rxjs';
import {
  debounceTime,
  distinctUntilChanged,
  map,
  mergeMap,
  reduce,
  startWith,
  switchMap,
  take,
} from 'rxjs/operators';

import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { CollectionDataService } from '../../core/data/collection-data.service';
import { FindListOptions } from '../../core/data/find-list-options.model';
import { PaginatedList } from '../../core/data/paginated-list.model';
import { RemoteData } from '../../core/data/remote-data';
import { Collection } from '../../core/shared/collection.model';
import { Community } from '../../core/shared/community.model';
import {
  getFirstCompletedRemoteData,
  getFirstSucceededRemoteDataPayload,
} from '../../core/shared/operators';
import { hasValue } from '../empty.util';
import { ThemedLoadingComponent } from '../loading/themed-loading.component';
import { followLink } from '../utils/follow-link-config.model';

/**
 * An interface to represent a collection entry
 */
interface CollectionListEntryItem {
  id: string;
  uuid: string;
  name: string;
}

/**
 * An interface to represent an entry in the collection list
 */
export interface CollectionListEntry {
  communities: CollectionListEntryItem[];
  collection: CollectionListEntryItem;
}

@Component({
  selector: 'ds-base-collection-dropdown',
  templateUrl: './collection-dropdown.component.html',
  styleUrls: ['./collection-dropdown.component.scss'],
  standalone: true,
  imports: [NgIf, FormsModule, ReactiveFormsModule, InfiniteScrollModule, NgFor, ThemedLoadingComponent, AsyncPipe, TranslateModule],
})
export class CollectionDropdownComponent implements OnInit, OnDestroy {

  /**
   * The search form control
   * @type {FormControl}
   */
  public searchField: UntypedFormControl = new UntypedFormControl();

  /**
   * The collection list obtained from a search
   * @type {Observable<CollectionListEntry[]>}
   */
  public searchListCollection$: Observable<CollectionListEntry[]>;

  /**
   * A boolean representing if dropdown list is scrollable to the bottom
   * @type {boolean}
   */
  private scrollableBottom = false;

  /**
   * A boolean representing if dropdown list is scrollable to the top
   * @type {boolean}
   */
  private scrollableTop = false;

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

  /**
   * The list of collection to render
   */
  searchListCollection: CollectionListEntry[] = [];

  @Output() selectionChange = new EventEmitter<CollectionListEntry>();
  /**
   * A boolean representing if the loader is visible or not
   */
  isLoading: BehaviorSubject<boolean> = new BehaviorSubject(false);

  /**
   * A numeric representing current page
   */
  currentPage: number;

  /**
   * A boolean representing if exist another page to render
   */
  hasNextPage: boolean;

  /**
   * Current search query used to filter collection list
   */
  currentQuery: string;

  /**
   * If present this value is used to filter collection list by entity type
   */
  @Input() entityType: string;

  /**
   * Emit to notify whether search is complete
   */
  @Output() searchComplete = new EventEmitter<any>();

  /**
   * Emit to notify the only selectable collection.
   */
  @Output() theOnlySelectable = new EventEmitter<CollectionListEntry>();

  constructor(
    private changeDetectorRef: ChangeDetectorRef,
    private collectionDataService: CollectionDataService,
    private el: ElementRef,
    public dsoNameService: DSONameService,
  ) { }

  /**
   * Method called on mousewheel event, it prevent the page scroll
   * when arriving at the top/bottom of dropdown menu
   *
   * @param event
   *     mousewheel event
   */
  @HostListener('mousewheel', ['$event']) onMousewheel(event) {
    if (event.wheelDelta > 0 && this.scrollableTop) {
      event.preventDefault();
    }
    if (event.wheelDelta < 0 && this.scrollableBottom) {
      event.preventDefault();
    }
  }

  /**
   * Initialize collection list
   */
  ngOnInit() {
    this.isLoading.next(false);
    this.subs.push(this.searchField.valueChanges.pipe(
      debounceTime(500),
      distinctUntilChanged(),
      startWith(''),
    ).subscribe(
      (next) => {
        if (hasValue(next) && next !== this.currentQuery) {
          this.resetPagination();
          this.currentQuery = next;
          this.populateCollectionList(this.currentQuery, this.currentPage);
        }
      },
    ));
    // Workaround for prevent the scroll of main page when this component is placed in a dialog
    setTimeout(() => this.el.nativeElement.querySelector('input').focus(), 0);
  }

  /**
   * Check if dropdown scrollbar is at the top or bottom of the dropdown list
   *
   * @param event
   */
  onScroll(event) {
    this.scrollableBottom = (event.target.scrollTop + event.target.clientHeight === event.target.scrollHeight);
    this.scrollableTop = (event.target.scrollTop === 0);
  }

  /**
   * Method used from infinity scroll for retrieve more data on scroll down
   */
  onScrollDown() {
    if ( this.hasNextPage ) {
      this.populateCollectionList(this.currentQuery, ++this.currentPage);
    }
  }

  /**
   * Emit a [selectionChange] event when a new collection is selected from list
   *
   * @param event
   *    the selected [CollectionListEntry]
   */
  onSelect(event: CollectionListEntry) {
    this.isLoading.next(true);
    this.selectionChange.emit(event);
  }

  /**
   * Method called for populate the collection list
   * @param query text for filter the collection list
   * @param page page number
   */
  populateCollectionList(query: string, page: number) {
    this.isLoading.next(true);
    // Set the pagination info
    const findOptions: FindListOptions = {
      elementsPerPage: 10,
      currentPage: page,
    };
    let searchListService$: Observable<RemoteData<PaginatedList<Collection>>>;
    if (this.entityType) {
      searchListService$ = this.collectionDataService
        .getAuthorizedCollectionByEntityType(
          query,
          this.entityType,
          findOptions,
          true,
          followLink('parentCommunity'));
    } else {
      searchListService$ = this.collectionDataService
        .getAuthorizedCollection(query, findOptions, true, true, followLink('parentCommunity'));
    }
    this.searchListCollection$ = searchListService$.pipe(
      getFirstCompletedRemoteData(),
      switchMap((collectionsRD: RemoteData<PaginatedList<Collection>>) => {
        this.searchComplete.emit();
        if (collectionsRD.hasSucceeded && collectionsRD.payload.totalElements > 0) {
          if (this.searchListCollection.length >= collectionsRD.payload.totalElements) {
            this.hasNextPage = false;
          }
          this.emitSelectionEvents(collectionsRD);
          return observableFrom(collectionsRD.payload.page).pipe(
            mergeMap((collection: Collection) => collection.parentCommunity.pipe(
              getFirstSucceededRemoteDataPayload(),
              map((community: Community) => ({
                communities: [{ id: community.id, name: this.dsoNameService.getName(community) }],
                collection: { id: collection.id, uuid: collection.id, name: this.dsoNameService.getName(collection) },
              }),
              ))),
            reduce((acc: any, value: any) => [...acc, value], []),
          );
        } else {
          this.hasNextPage = false;
          return observableOf([]);
        }
      }),
    );
    this.subs.push(
      this.searchListCollection$.subscribe((list: CollectionListEntry[]) => {
        this.searchListCollection.push(...list);
        this.hideShowLoader(false);
        this.changeDetectorRef.detectChanges();
      }),
    );
  }

  /**
   * Unsubscribe from all subscriptions
   */
  ngOnDestroy(): void {
    this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
  }

  /**
   * Reset search form control
   */
  reset() {
    this.searchField.setValue('');
  }

  /**
   * Reset pagination values
   */
  resetPagination() {
    this.currentPage = 1;
    this.currentQuery = '';
    this.hasNextPage = true;
    this.searchListCollection = [];
  }

  /**
   * Hide/Show the collection list loader
   * @param hideShow true for show, false otherwise
   */
  hideShowLoader(hideShow: boolean) {
    this.isLoading.next(hideShow);
  }

  /**
   * Emit events related to the number of selectable collections.
   * hasChoice containing whether there are more then one selectable collections.
   * theOnlySelectable containing the only collection available.
   * @param collections
   * @private
   */
  private emitSelectionEvents(collections: RemoteData<PaginatedList<Collection>>) {
    if (collections.payload.totalElements === 1) {
      const collection = collections.payload.page[0];
      collections.payload.page[0].parentCommunity.pipe(
        getFirstSucceededRemoteDataPayload(),
        take(1),
      ).subscribe((community: Community) => {
        this.theOnlySelectable.emit({
          communities: [{ id: community.id, name: this.dsoNameService.getName(community), uuid: community.id }],
          collection: { id: collection.id, uuid: collection.id, name: this.dsoNameService.getName(collection) },
        });
      });
    }
  }

}