import {
  Component,
  Input,
  OnInit,
  ViewChild,
  ViewContainerRef, OnDestroy,
} from '@angular/core';
import { Bundle } from '../../../../core/shared/bundle.model';
import { Item } from '../../../../core/shared/item.model';
import { ResponsiveColumnSizes } from '../../../../shared/responsive-table-sizes/responsive-column-sizes';
import { ResponsiveTableSizes } from '../../../../shared/responsive-table-sizes/responsive-table-sizes';
import { getItemPageRoute } from '../../../item-page-routing-paths';
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
import { RemoteData } from 'src/app/core/data/remote-data';
import { PaginatedList } from 'src/app/core/data/paginated-list.model';
import { Bitstream } from 'src/app/core/shared/bitstream.model';
import { Observable, BehaviorSubject, switchMap, shareReplay, Subscription } from 'rxjs';
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
import { FieldUpdates } from '../../../../core/data/object-updates/field-updates.model';
import { PaginatedSearchOptions } from '../../../../shared/search/models/paginated-search-options.model';
import { BundleDataService } from '../../../../core/data/bundle-data.service';
import { followLink } from '../../../../shared/utils/follow-link-config.model';
import {
  getAllSucceededRemoteData,
  paginatedListToArray,
} from '../../../../core/shared/operators';
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
import { map, take, filter, tap } from 'rxjs/operators';
import { FieldChangeType } from '../../../../core/data/object-updates/field-change-type.model';
import { FieldUpdate } from '../../../../core/data/object-updates/field-update.model';
import { PaginationService } from '../../../../core/pagination/pagination.service';
import { PaginationComponent } from '../../../../shared/pagination/pagination.component';
import { RequestService } from '../../../../core/data/request.service';
import {
  ItemBitstreamsService,
  BitstreamTableEntry,
  SelectedBitstreamTableEntry,
  MOVE_KEY, SelectionAction
} from '../item-bitstreams.service';
import { CdkDragDrop } from '@angular/cdk/drag-drop';
import { hasValue, hasNoValue } from '../../../../shared/empty.util';

@Component({
  selector: 'ds-item-edit-bitstream-bundle',
  styleUrls: ['../item-bitstreams.component.scss', './item-edit-bitstream-bundle.component.scss'],
  templateUrl: './item-edit-bitstream-bundle.component.html',
})
/**
 * Component that displays a single bundle of an item on the item bitstreams edit page
 * Creates an embedded view of the contents. This is to ensure the table structure won't break.
 * (which means it'll be added to the parents html without a wrapping ds-item-edit-bitstream-bundle element)
 */
export class ItemEditBitstreamBundleComponent implements OnInit, OnDestroy {
  protected readonly FieldChangeType = FieldChangeType;

  /**
   * The view on the bundle information and bitstreams
   */
  @ViewChild('bundleView', {static: true}) bundleView;

  /**
   * The view on the pagination component
   */
  @ViewChild(PaginationComponent) paginationComponent: PaginationComponent;

  /**
   * The view on the drag tooltip
   */
  @ViewChild('dragTooltip') dragTooltip;

  /**
   * The bundle to display bitstreams for
   */
  @Input() bundle: Bundle;

  /**
   * The item the bundle belongs to
   */
  @Input() item: Item;

  /**
   * The bootstrap sizes used for the columns within this table
   */
  @Input() columnSizes: ResponsiveTableSizes;

  /**
   * Whether this is the first in a series of bundle tables
   */
  @Input() isFirstTable = false;

  /**
   * The bootstrap sizes used for the Bundle Name column
   * This column stretches over the first 3 columns and thus is a combination of their sizes processed in ngOnInit
   */
  bundleNameColumn: ResponsiveColumnSizes;

  /**
   * Route to the item's page
   */
  itemPageRoute: string;

  /**
   * The name of the bundle
   */
  bundleName: string;

  /**
   * The number of bitstreams in the bundle
   */
  bundleSize: number;

  /**
   * The bitstreams to show in the table
   */
  bitstreamsRD$: Observable<RemoteData<PaginatedList<Bitstream>>>;

  /**
   * The data to show in the table
   */
  tableEntries$: BehaviorSubject<BitstreamTableEntry[]> = new BehaviorSubject([]);

  /**
   * The initial page options to use for fetching the bitstreams
   */
  paginationOptions: PaginationComponentOptions;

  /**
   * The current page options
   */
  currentPaginationOptions$: BehaviorSubject<PaginationComponentOptions>;

  /**
   * The currently selected page size
   */
  pageSize$: BehaviorSubject<number>;

  /**
   * The self url of the bundle, also used when retrieving fieldUpdates
   */
  bundleUrl: string;

  /**
   * The updates to the current bitstreams
   */
  updates$: BehaviorSubject<FieldUpdates> = new BehaviorSubject(null);

  /**
   * Array containing all subscriptions created by this component
   */
  subscriptions: Subscription[] = [];


  constructor(
    protected viewContainerRef: ViewContainerRef,
    public dsoNameService: DSONameService,
    protected bundleService: BundleDataService,
    protected objectUpdatesService: ObjectUpdatesService,
    protected paginationService: PaginationService,
    protected requestService: RequestService,
    protected itemBitstreamsService: ItemBitstreamsService,
  ) {
  }

  ngOnInit(): void {
    this.bundleNameColumn = this.columnSizes.combineColumns(0, 2);
    this.viewContainerRef.createEmbeddedView(this.bundleView);
    this.itemPageRoute = getItemPageRoute(this.item);
    this.bundleName = this.dsoNameService.getName(this.bundle);
    this.bundleUrl = this.bundle.self;

    this.initializePagination();
    this.initializeBitstreams();
    this.initializeSelectionActions();
  }

  ngOnDestroy() {
    this.viewContainerRef.clear();
    this.subscriptions.forEach(sub => sub?.unsubscribe());
  }

  protected initializePagination() {
    this.paginationOptions = this.itemBitstreamsService.getInitialBitstreamsPaginationOptions(this.bundleName);

    this.currentPaginationOptions$ = new BehaviorSubject(this.paginationOptions);
    this.pageSize$ = new BehaviorSubject(this.paginationOptions.pageSize);

    this.subscriptions.push(
      this.paginationService.getCurrentPagination(this.paginationOptions.id, this.paginationOptions)
        .subscribe((pagination) => {
          this.currentPaginationOptions$.next(pagination);
          this.pageSize$.next(pagination.pageSize);
        })
    );

  }

  protected initializeBitstreams() {
    this.bitstreamsRD$ = this.currentPaginationOptions$.pipe(
      switchMap((page: PaginationComponentOptions) => {
        const paginatedOptions = new PaginatedSearchOptions({ pagination: Object.assign({}, page) });
        return this.bundleService.getBitstreamsEndpoint(this.bundle.id, paginatedOptions).pipe(
          switchMap((href) => this.requestService.hasByHref$(href)),
          switchMap(() => this.bundleService.getBitstreams(
            this.bundle.id,
            paginatedOptions,
            followLink('format')
          ))
        );
      }),
      getAllSucceededRemoteData(),
      shareReplay(1),
    );

    this.subscriptions.push(
      this.bitstreamsRD$.pipe(
        take(1),
        tap(bitstreamsRD => this.bundleSize = bitstreamsRD.payload.totalElements),
        paginatedListToArray(),
      ).subscribe((bitstreams) => {
        this.objectUpdatesService.initialize(this.bundleUrl, bitstreams, new Date());
      }),

      this.bitstreamsRD$.pipe(
        paginatedListToArray(),
        switchMap((bitstreams) => this.objectUpdatesService.getFieldUpdatesExclusive(this.bundleUrl, bitstreams))
      ).subscribe((updates) => this.updates$.next(updates)),

      this.bitstreamsRD$.pipe(
        paginatedListToArray(),
        map((bitstreams) => this.itemBitstreamsService.mapBitstreamsToTableEntries(bitstreams)),
      ).subscribe((tableEntries) => this.tableEntries$.next(tableEntries)),
    );
  }

  protected initializeSelectionActions() {
    this.subscriptions.push(
      this.itemBitstreamsService.getSelectionAction$().subscribe(
        selectionAction => this.handleSelectionAction(selectionAction))
    );
  }

  /**
   * Handles a change in selected bitstream by changing the pagination if the change happened on a different page
   * @param selectionAction
   */
  handleSelectionAction(selectionAction: SelectionAction) {
    if (hasNoValue(selectionAction) || selectionAction.selectedEntry.bundle !== this.bundle) {
      return;
    }

    if (selectionAction.action === 'Moved') {
      // If the currently selected bitstream belongs to this bundle, it has possibly moved to a different page.
      // In that case we want to change the pagination to the new page.
      this.redirectToCurrentPage(selectionAction.selectedEntry);
    }

    if (selectionAction.action === 'Cancelled') {
      // If the selection is cancelled (and returned to its original position), it is possible the previously selected
      // bitstream is returned to a different page. In that case we want to change the pagination to the place where
      // the bitstream was returned to.
      this.redirectToOriginalPage(selectionAction.selectedEntry);
    }

    if (selectionAction.action === 'Cleared') {
      // If the selection is cleared, it is possible the previously selected bitstream is on a different page. In that
      // case we want to change the pagination to the place where the bitstream is.
      this.redirectToCurrentPage(selectionAction.selectedEntry);
    }
  }

  /**
   * Redirect the user to the current page of the provided bitstream if it is on a different page.
   * @param bitstreamEntry The entry that the current position will be taken from to determine the page to move to
   * @protected
   */
  protected redirectToCurrentPage(bitstreamEntry: SelectedBitstreamTableEntry) {
    const currentPage = this.getCurrentPage();
    const selectedEntryPage = this.bundleIndexToPage(bitstreamEntry.currentPosition);

    if (currentPage !== selectedEntryPage) {
      this.changeToPage(selectedEntryPage);
    }
  }

  /**
   * Redirect the user to the original page of the provided bitstream if it is on a different page.
   * @param bitstreamEntry The entry that the original position will be taken from to determine the page to move to
   * @protected
   */
  protected redirectToOriginalPage(bitstreamEntry: SelectedBitstreamTableEntry) {
    const currentPage = this.getCurrentPage();
    const originPage = this.bundleIndexToPage(bitstreamEntry.originalPosition);

    if (currentPage !== originPage) {
      this.changeToPage(originPage);
    }
  }

  /**
   * Check if a user should be allowed to remove this field
   */
  canRemove(fieldUpdate: FieldUpdate): boolean {
    return fieldUpdate.changeType !== FieldChangeType.REMOVE;
  }

  /**
   * Check if a user should be allowed to cancel the update to this field
   */
  canUndo(fieldUpdate: FieldUpdate): boolean {
    return fieldUpdate.changeType >= 0;
  }

  /**
   * Sends a new remove update for this field to the object updates service
   */
  remove(bitstream: Bitstream): void {
    this.objectUpdatesService.saveRemoveFieldUpdate(this.bundleUrl, bitstream);
  }

  /**
   * Cancels the current update for this field in the object updates service
   */
  undo(bitstream: Bitstream): void {
    this.objectUpdatesService.removeSingleFieldUpdate(this.bundleUrl, bitstream.uuid);
  }

  /**
   * Returns the css class for a table row depending on the state of the table entry.
   * @param update
   * @param bitstream
   */
  getRowClass(update: FieldUpdate, bitstream: BitstreamTableEntry): string {
    const selected = this.itemBitstreamsService.getSelectedBitstream();

    if (hasValue(selected) && bitstream.id === selected.bitstream.id) {
      return 'table-info';
    }

    switch (update.changeType) {
      case FieldChangeType.UPDATE:
        return 'table-warning';
      case FieldChangeType.ADD:
        return 'table-success';
      case FieldChangeType.REMOVE:
        return 'table-danger';
      default:
        return 'bg-white';
    }
  }

  /**
   * Changes the page size to the provided page size.
   * @param pageSize
   */
  public doPageSizeChange(pageSize: number) {
    this.paginationComponent.doPageSizeChange(pageSize);
  }

  /**
   * Handles start of dragging by opening the tooltip mentioning that it is possible to drag a bitstream to a different
   * page by dropping it on the page number, only if there are multiple pages.
   */
  dragStart() {
    // Only open the drag tooltip when there are multiple pages
    this.paginationComponent.shouldShowBottomPager.pipe(
      take(1),
      filter((hasMultiplePages) => hasMultiplePages),
    ).subscribe(() => {
      this.dragTooltip.open();
    });
  }

  /**
   * Handles end of dragging by closing the tooltip.
   */
  dragEnd() {
    this.dragTooltip.close();
  }

  /**
   * Handles dropping by calculation the target position, and changing the page if the bitstream was dropped on a
   * different page.
   * @param event
   */
  drop(event: CdkDragDrop<any>) {
    const dragIndex = event.previousIndex;
    let dropIndex = event.currentIndex;
    const dragPage = this.getCurrentPage();
    let dropPage = this.getCurrentPage();

    // Check if the user is hovering over any of the pagination's pages at the time of dropping the object
    const droppedOnElement = document.elementFromPoint(event.dropPoint.x, event.dropPoint.y);
    if (hasValue(droppedOnElement) && hasValue(droppedOnElement.textContent) && droppedOnElement.classList.contains('page-link')) {
      // The user is hovering over a page, fetch the page's number from the element
      let droppedPage = Number(droppedOnElement.textContent);
      if (hasValue(droppedPage) && !Number.isNaN(droppedPage)) {
        droppedPage -= 1;

        if (droppedPage !== dragPage) {
          dropPage = droppedPage;

          if (dropPage > dragPage) {
            // When moving to later page, place bitstream at the top
            dropIndex = 0;
          } else {
            // When moving to earlier page, place bitstream at the bottom
            dropIndex = this.getCurrentPageSize() - 1;
          }
        }
      }
    }

    const fromIndex = this.pageIndexToBundleIndex(dragIndex, dragPage);
    const toIndex = this.pageIndexToBundleIndex(dropIndex, dropPage);

    if (fromIndex === toIndex) {
      return;
    }

    const selectedBitstream = this.tableEntries$.value[dragIndex];

    const finish = () => {
      this.itemBitstreamsService.announceMove(selectedBitstream.name, toIndex);

      if (dropPage !== this.getCurrentPage()) {
        this.changeToPage(dropPage);
      }

      this.itemBitstreamsService.displaySuccessNotification(MOVE_KEY);
    };

    this.itemBitstreamsService.performBitstreamMoveRequest(this.bundle, fromIndex, toIndex, finish);
  }

  /**
   * Handles a select action for the provided bitstream entry.
   * If the selected bitstream is currently selected, the selection is cleared.
   * If no, or a different bitstream, is selected, the provided bitstream becomes the selected bitstream.
   * @param event The event that triggered the select action
   * @param bitstream The bitstream that is the target of the select action
   */
  select(event: UIEvent, bitstream: BitstreamTableEntry) {
    event.preventDefault();

    if (event instanceof KeyboardEvent && event.repeat) {
      // Don't handle hold events, otherwise it would change rapidly between being selected and unselected
      return;
    }

    const selectedBitstream = this.itemBitstreamsService.getSelectedBitstream();

    if (hasValue(selectedBitstream) && selectedBitstream.bitstream === bitstream) {
      this.itemBitstreamsService.cancelSelection();
    } else {
      const selectionObject = this.createBitstreamSelectionObject(bitstream);

      if (hasNoValue(selectionObject)) {
        console.warn('Failed to create selection object');
        return;
      }

      this.itemBitstreamsService.selectBitstreamEntry(selectionObject);
    }
  }

  /**
   * Creates a {@link SelectedBitstreamTableEntry} from the provided {@link BitstreamTableEntry} so it can be given
   * to the {@link ItemBitstreamsService} to select the table entry.
   * @param bitstream The table entry to create the selection object from.
   * @protected
   */
  protected createBitstreamSelectionObject(bitstream: BitstreamTableEntry): SelectedBitstreamTableEntry {
    const pageIndex = this.findBitstreamPageIndex(bitstream);

    if (pageIndex === -1) {
      return null;
    }

    const position = this.pageIndexToBundleIndex(pageIndex, this.getCurrentPage());

    return {
      bitstream: bitstream,
      bundle: this.bundle,
      bundleSize: this.bundleSize,
      currentPosition: position,
      originalPosition: position,
    };
  }

  /**
   * Returns the index of the provided {@link BitstreamTableEntry} relative to the current page
   * If the current page size is 10, it will return a value from 0 to 9 (inclusive)
   * Returns -1 if the provided bitstream could not be found
   * @protected
   */
  protected findBitstreamPageIndex(bitstream: BitstreamTableEntry): number {
    const entries = this.tableEntries$.value;
    return entries.findIndex(entry => entry === bitstream);
  }

  /**
   * Returns the current zero-indexed page
   * @protected
   */
  protected getCurrentPage(): number {
    // The pagination component uses one-based numbering while zero-based numbering is more convenient for calculations
    return this.currentPaginationOptions$.value.currentPage - 1;
  }

  /**
   * Returns the current page size
   * @protected
   */
  protected getCurrentPageSize(): number {
    return this.currentPaginationOptions$.value.pageSize;
  }

  /**
   * Converts an index relative to the page to an index relative to the bundle
   * @param index The index relative to the page
   * @param page  The zero-indexed page number
   * @protected
   */
  protected pageIndexToBundleIndex(index: number, page: number) {
    return page * this.getCurrentPageSize() + index;
  }

  /**
   * Calculates the zero-indexed page number from the index relative to the bundle
   * @param index The index relative to the bundle
   * @protected
   */
  protected bundleIndexToPage(index: number) {
    return Math.floor(index / this.getCurrentPageSize());
  }

  /**
   * Change the pagination for this bundle to the provided zero-indexed page
   * @param page The zero-indexed page to change to
   * @protected
   */
  protected changeToPage(page: number) {
    // Increments page by one because zero-indexing is way easier for calculations but the pagination component
    // uses one-indexing.
    this.paginationComponent.doPageChange(page + 1);
  }
}
