/*
 * Copyright (C) 2019 Robert Bosch GmbH Copyright (C) 2019 Robert Bosch Manufacturing Solutions GmbH, Germany. All rights reserved.
 */

import {
  AfterViewInit,
  Component,
  EventEmitter,
  Input,
  OnDestroy,
  OnInit,
  Output,
  QueryList,
  TemplateRef,
  ViewChildren,
} from '@angular/core';
import { DataSource } from '@angular/cdk/table';
import { ActiveDescendantKeyManager } from '@angular/cdk/a11y';
import { ENTER, TAB } from '@angular/cdk/keycodes';
import { BehaviorSubject, Observable, of, Subject, Subscription } from 'rxjs';
import { takeUntil, tap, filter } from 'rxjs/operators';
import {
  Column,
  ColumnItem,
  ColumnsDataSource,
  BciColumnBrowserModel,
  ColumnFilter,
  SelectionChange,
} from './column-browser.model';
import { ColumnBrowserService } from './column-browser.service';
import { ColumnDirective } from './column.directive';
import { ColumnItemDirective } from './column-item.directive';
import { CollectionViewer } from '@angular/cdk/collections';

/**
 * A column browser component that visualizes tree-like models through concatenated columns. In contrast to a tree view,
 * the control visualizes a single selection path at a time.
 *
 * @description
 * The column browser requires a data source of type {@link ColumnsDataSource}. Use either an observable or a data
 * source if the data is subject to changes. Currently, the data browser requires the data type to be the same for each
 * level, i.e. hierarchical data of different types is not support yet.
 *
 * Each column can have a distinct title. Add those via the {@link ColumnBrowserComponent#titles} input.
 *
 * The component supports several keyboard shortcuts by passing down all keyboard events using the
 * {@link ColumnBrowserComponent#keyboardEvents} input. These are:
 * * `ENTER` / `TAB` selects an item, if the item has children another column will be added
 * * `ARROW UP` / `DOWN` changes the selected item of the active column
 *
 * Filter the column items by passing in a list of filter strings using the {@link ColumnBrowserComponent#filter} input.
 */
@Component({
  selector: 'bci-column-browser',
  templateUrl: './column-browser.component.html',
  styleUrls: ['./column-browser.component.scss'],
  providers: [ColumnBrowserService],
})
export class ColumnBrowserComponent implements OnInit, OnDestroy, CollectionViewer, AfterViewInit {
  /** All columns that have visible children. */
  get visibleColumns(): Column[] {
    return this.columns.filter((column) => column.items.some((item) => item.visible));
  }

  @Input() get dataSource(): ColumnsDataSource {
    return this._dataSource;
  }

  set dataSource(dataSource: ColumnsDataSource) {
    if (this._dataSource !== dataSource) {
      this.switchDataSource(dataSource);
    }
  }

  /** Current selection path through the columns. */
  @Input() set selectedPath(value: any[]) {
    this._selectedPath = value;
    this.applySelectedPath();
  }

  get selectedPath(): any[] {
    return this._selectedPath;
  }

  /** Titles of all columns. */
  @Input() set titles(value: string[]) {
    this._titles = value;
    this.service.updateHeaders(this.titles);
  }

  get titles(): string[] {
    return this._titles;
  }

  viewChange = new BehaviorSubject<{ start: number; end: number }>({ start: 0, end: Number.MAX_VALUE });

  /** All column items of every visible column. */
  @ViewChildren(ColumnItemDirective) listItems: QueryList<ColumnItemDirective>;

  /** All visible columns. */
  @ViewChildren(ColumnDirective) lists: QueryList<ColumnDirective>;

  /** Optional template that overwrites the display of {@link BciColumnBrowserModel#label}. */
  @Input() columnItemTemplate: TemplateRef<any>;

  /** Whether all columns / user input is disabled. Applies a shading to highlight the disabled state. */
  @Input() disabled = false;

  /** Required in typeahead mode. All keyboard events must be forwarded. */
  @Input() keyboardEvents: Subject<{ index: number; keyEvent: KeyboardEvent }>;

  /** Notify column browser if filter specific to a column has been set. */
  @Input() filter = new Observable<ColumnFilter>();

  /** Event that is emitted when the selection path changes. */
  @Output() readonly selectedPathChange = new EventEmitter<BciColumnBrowserModel[]>();

  /** Event that is emitted when the selection state of a single item changes. */
  @Output() readonly selectionChange = new EventEmitter<SelectionChange>();

  /** Filter for every column defaulting to empty string. */
  private columnFilter = [''];

  private _titles: string[] = [];

  private _selectedPath: any[] = [];

  private columns: Column[] = [];

  private _dataSource: ColumnsDataSource;

  /** Each column has its own key manager. */
  private keyManager: ActiveDescendantKeyManager<ColumnItemDirective>[] = [];

  private _dataSubscription: Subscription | null;

  private ngUnsubscribe = new Subject<void>();

  constructor(private service: ColumnBrowserService) {}

  ngOnInit(): void {
    this.service.columns.pipe(takeUntil(this.ngUnsubscribe)).subscribe((cols: Column[]) => {
      this.columns = cols;
    });

    if (this.keyboardEvents) {
      this.keyboardEvents
        .pipe(takeUntil(this.ngUnsubscribe))
        .subscribe((value: { index: number; keyEvent: KeyboardEvent }) => {
          if (this.keyManager.length > value.index) {
            this.onKeyDown(value.keyEvent, value.index);
          }
        });
    }

    this.filter
      .pipe(
        tap((value: ColumnFilter) => (this.columnFilter = value.filter)),
        filter((value: ColumnFilter) => value.filter[value.columnIndex] === ''),
        takeUntil(this.ngUnsubscribe)
      )
      .subscribe((value: ColumnFilter) => {
        this.columnFilter.splice(value.columnIndex + 1);
        this.service.clearColumnSelection(value.columnIndex);
        this.keyManager[value.columnIndex].setActiveItem(null);
        this.resetColumns(value.columnIndex + 1);
      });
  }

  ngOnDestroy(): void {
    this.ngUnsubscribe.next();
    this.ngUnsubscribe.complete();
  }

  ngAfterViewInit(): void {
    // Recreate key managers every time the columns change because `ActiveDescendantKeyManager` won't allow to directly
    // update its internal list of items
    this.listItems.changes.pipe(takeUntil(this.ngUnsubscribe)).subscribe((items: QueryList<ColumnItemDirective>) => {
      this.keyManager = [];
      this.columns.forEach((column, index) => {
        this.keyManager.push(
          new ActiveDescendantKeyManager<ColumnItemDirective>(
            items.filter((item) => item.columnLevel === index)
          ).withWrap()
        );
      });
    });
  }

  onItemClick(columnItem: ColumnItem, level: number): void {
    this.toggleSelectionState(columnItem, level);
  }

  onKeyDown(event: KeyboardEvent, level: number): void {
    // tslint:disable-next-line:deprecation
    if (event.keyCode === ENTER || event.keyCode === TAB) {
      // Pressing tab moves to the next facility level's input and hence requires to select the highlighted item
      if (this.keyManager[level].activeItem && this.keyManager[level].activeItem.item) {
        this.toggleSelectionState(this.keyManager[level].activeItem.item, level, true);
      }
    } else {
      this.keyManager[level].onKeydown(event);

      if (!this.keyManager[level].activeItemIndex) {
        this.keyManager[level].setActiveItem(0);
      }

      const listItem = this.listItems.find(
        (item) => item.columnLevel === level && item === this.keyManager[level].activeItem
      );
      const colList = this.lists.find((list) => list.columnIndex === level);
      this.scrollToItem(colList, listItem, this.keyManager[level].activeItemIndex || 0);
    }
  }

  /**
   * Returns all visible items of a column. The items are filtered based on
   * {@link ColumnBrowserComponent#columnFilter the filter expression of this column}.
   * @param level Current column level
   */
  getAvailableItems(level: number): ColumnItem[] {
    if (!this.columns[level]) {
      return [];
    }
    const items = this.columns[level].items.filter((item) => item.visible === true);
    if (this.columnFilter[level]) {
      return items.filter((item) => item.label.toLowerCase().startsWith(this.columnFilter[level].toLowerCase()));
    }
    return items;
  }

  /**
   * Returns the number of items of the {@param level given column}.
   * @param level
   */
  getColumnItemCount(level: number): number {
    if (level < this.columns.length) {
      return this.columns[level].items.filter((item) => item.visible === true).length;
    }
    return -1;
  }

  resetColumns(level: number): void {
    this.service.resetColumns(level);
  }

  /** Highlights and scrolls to the selected items of each column. */
  updateSelections(): void {
    for (let columnIndex = 0; columnIndex < this.columns.length; columnIndex++) {
      const listItem = this.listItems.find((item) => item.columnLevel === columnIndex && item.item.selected);
      const colList = this.lists.find((list) => list.columnIndex === columnIndex);

      if (columnIndex < this.keyManager.length) {
        this.keyManager[columnIndex].setActiveItem(listItem);
      }

      if (colList && listItem) {
        this.scrollToItem(colList, listItem, listItem.index || 0);
      }
    }
  }

  private scrollToItem(column: ColumnDirective, columnItem: ColumnItemDirective, index: number): void {
    if (!columnItem || !column) {
      return;
    }
    column.scrollToItem(columnItem, index);
  }

  /** Updates the selected path in the columns. */
  private updateSelectedPath(): void {
    this.selectedPath = [];
    this.columns.forEach((item) => {
      const selected = item.items.find((i) => i.selected);
      if (selected) {
        this.selectedPath.push(selected.item.model);
      }
    });
    this.selectedPathChange.emit(this.selectedPath);
  }

  /**
   * Selects or deselects the {@param columnItem given column}.
   * @param columnItem
   * @param level The {@param columnItem column item's} level.
   * @param preventDeselection
   */
  private toggleSelectionState(columnItem: ColumnItem, level: number, preventDeselection = false): void {
    if (columnItem.selected && preventDeselection) {
      return;
    }

    let selected = false;
    if (columnItem.selected) {
      // deselect
      this.service.deselectColumnItem(columnItem, level);
    } else {
      // select
      selected = true;
      this.service.updateColumns(columnItem, level, true);
      if (this.keyManager.length > level) {
        this.keyManager[level].setActiveItem(null);
      }
    }
    this.updateSelectedPath();
    const selectionChangeValue: SelectionChange = {
      columnIndex: level,
      selected: selected,
      item: columnItem.item,
      selectedPath: this.selectedPath,
    };
    this.selectionChange.emit(selectionChangeValue);
  }

  /** Applies the selection state of all column items according to {@link ColumnBrowserComponent#selectedPath}. */
  private applySelectedPath(): void {
    if (!this.selectedPath) {
      return;
    }
    this.columns.forEach((col, index) => {
      col.items.forEach((colItem) => {
        if (
          this.selectedPath[index] &&
          JSON.stringify(colItem.item.model) === JSON.stringify(this.selectedPath[index])
        ) {
          this.service.updateColumns(colItem, index);
        }
      });
    });
  }

  private switchDataSource(dataSource: ColumnsDataSource): void {
    if (this._dataSource && typeof (this._dataSource as DataSource<BciColumnBrowserModel>).disconnect === 'function') {
      (this.dataSource as DataSource<BciColumnBrowserModel>).disconnect(this);
    }

    if (this._dataSubscription) {
      this._dataSubscription.unsubscribe();
      this._dataSubscription = null;
    }

    this._dataSource = dataSource;
    this.observeRenderChanges();
  }

  private observeRenderChanges(): void {
    let dataStream: Observable<BciColumnBrowserModel[]> | undefined;

    if (typeof (this._dataSource as DataSource<BciColumnBrowserModel>).connect === 'function') {
      dataStream = (this._dataSource as DataSource<BciColumnBrowserModel>).connect(this) as Observable<
        BciColumnBrowserModel[]
      >;
    } else if (this._dataSource instanceof Observable) {
      dataStream = this._dataSource;
    } else if (Array.isArray(this._dataSource)) {
      dataStream = of(this._dataSource);
    }

    if (dataStream) {
      this._dataSubscription = dataStream.pipe(takeUntil(this.ngUnsubscribe)).subscribe((data) => {
        this.service.initColumns(data, this.titles, this.selectedPath);
      });
    }
  }
}
