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

import {
  ChangeDetectorRef,
  Component,
  EventEmitter,
  Input,
  OnDestroy,
  OnInit,
  Output,
  TemplateRef,
} from '@angular/core';
import { combineLatest, Observable, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';

import { BciPicklistIntl } from '../picklist-intl';
import { DragInstance, DragState } from '../picklist.model';
import { PicklistService } from '../picklist.service';
import {
  BciPicklistCompare,
  BciPicklistFilter,
  BciPicklistPicklistMode,
  BciPicklistResponsibility,
  BciPicklistStringFilter,
} from '../picklist.types';

/**
 * A picklist component for selecting items from a list of available items. The list of available items is referred to as source, and
 * the list of selected as target list. Both source and target list as well as this component interact using the {@link PicklistService}.
 */
@Component({
  selector: 'bci-picklist',
  templateUrl: './picklist.component.html',
  styleUrls: ['./picklist.component.scss'],
  providers: [PicklistService],
})
export class PicklistComponent implements OnInit, OnDestroy {
  /** Key up event emitter of the default source filter input. */
  sourceFilterKeyup = new EventEmitter<string>();

  /** Key up event emitter of the default target filter input. */
  targetFilterKeyup = new EventEmitter<string>();

  /** Whether some source items are available. */
  isSourceItemsAvailable = false;

  /** Whether some target items are available. */
  isTargetItemsAvailable = false;

  /** Whether some source items are selected. */
  isSomeSourceItemSelected = false;

  /** Whether some target items are selected. */
  isSomeTargetItemSelected = false;

  /** Wheter the current source selection including current target items count would exceed the target items max limit. */
  wouldSourceSelectionExceedTargetItemsMaxLimit = false;

  dragState: DragInstance = {
    state: DragState.Idle,
    source: 'none',
    draggedItems: [],
  };

  /** Hides the source filter input. */
  get disableSourceFilter(): boolean {
    return typeof this.sourceFilter === 'boolean' && !this.sourceFilter;
  }

  /** Hides the target filter input. */
  get disableTargetFilter(): boolean {
    return typeof this.targetFilter === 'boolean' && !this.targetFilter;
  }

  /** Template for the source list filter. */
  @Input() sourceFilterTemplate: TemplateRef<any>;

  /** Template for the target list filter. */
  @Input() targetFilterTemplate: TemplateRef<any>;

  /** List of source items. */
  get source(): any[] {
    return this._source;
  }

  @Input() set source(value: any[]) {
    this._source = value;
    this.picklistService.setSourceItems(value, false);
    this.picklistService.selectSourceItems(this._sourceSelected);
  }

  /** List of source selected items. */
  get sourceSelected(): any[] {
    return this._sourceSelected;
  }

  @Input() set sourceSelected(value: any[]) {
    this._sourceSelected = value;
    this.picklistService.selectSourceItems(value);
  }

  _targetItemsMaxLimit: number;

  /**
   * Defines a specified maximum limit for target items.
   * If not defined, no limit is applied.
   */
  @Input() set targetItemsMaxLimit(value: number) {
    this._targetItemsMaxLimit = value;
    this.picklistService.setTargetItemsMaxLimit(value);
  }

  /** List of target items. */
  get target(): any[] {
    return this._target;
  }

  @Input() set target(value: any[]) {
    this._target = value;
    this.picklistService.setTargetItems(value, false);
    this.picklistService.selectTargetItems(this._targetSelected);
  }

  /** List of target selected items. */
  get targetSelected(): any[] {
    return this._targetSelected;
  }

  @Input() set targetSelected(value: any[]) {
    this._targetSelected = value;
    this.picklistService.selectTargetItems(value);
  }

  /**
   * Property that is displayed in the source or target list, depending if a source or target item template have been defined.
   * Ignored if a source or target item template has been specified.
   */
  @Input() displayProperty: string;

  /** Template for items in the source list. This template definition is independent from the target template. */
  @Input() sourceItemTemplate: TemplateRef<any>;

  /** Template for items in the target list. This template definition is independent from the source template. */
  @Input() targetItemTemplate: TemplateRef<any>;

  /** Defines whether elements are copied or moved from the list of source items to the list of target items. */
  @Input() mode: BciPicklistPicklistMode = 'move';

  /** Custom source list filter predicate. If not defined, the label is used for filtering. */
  @Input() sourceFilter: boolean | string | BciPicklistStringFilter | Observable<BciPicklistFilter> = true; // TODO Restore: Replace with sourceFilter
  // @Input() sourceFilter: boolean | string | Observable<BciPicklistFilter> = true;

  /** Custom target list filter predicate. If not defined, the label is used for filtering. */
  @Input() targetFilter: boolean | string | BciPicklistStringFilter | Observable<BciPicklistFilter> = true; // TODO Restore: Replace with targetFilter
  // @Input() targetFilter: boolean | string | Observable<BciPicklistFilter> = true;

  /**
   * Sort behavior of the source list. If set to undefined sort is disabled, if set to true the list will be sorted alphabetically,
   * or if a comparison function is specified, it is evaluated. If using a comparison function return -1 if the first element should
   * appear below the second element, 0 if both elements are equal, or 1 if the second element should appear below the first element.
   */
  get sourceSorting(): boolean | BciPicklistCompare {
    if (this._sourceSorting === 'asc' || this._sourceSorting === 'desc') {
      return true;
    } else if (this._sourceSorting === 'none') {
      return false;
    } else {
      return this._sourceSorting as BciPicklistCompare;
    }
  }

  // TODO Restore: Replace with _sourceSorting
  @Input() set sourceSorting(value: boolean | BciPicklistCompare) {
    if (typeof value === 'boolean') {
      if (value) {
        this._sourceSorting = 'asc';
      } else {
        this._sourceSorting = 'none';
      }
    } else {
      this._sourceSorting = value;
    }
  }

  /**
   * Sort behavior of the source list. If a comparison function is specified, it is evaluated. If using a comparison function return
   * -1 if the first element should appear below the second element, 0 if both elements are equal, or 1 if the second element should
   * appear below the first element.
   */
  _sourceSorting: 'none' | 'asc' | 'desc' | BciPicklistCompare = 'asc'; // TODO Restore: Use 'none' as default

  /**
   * Sort behavior of the target list. If set to undefined sort is disabled, if set to true the list will be sorted alphabetically,
   * or if a comparison function is specified, it is evaluated. If using a comparison function return -1 if the first element should
   * appear below the second element, 0 if both elements are equal, or 1 if the second element should appear below the first element.
   */
  get targetSorting(): boolean | BciPicklistCompare {
    if (this._targetSorting === 'asc' || this._targetSorting === 'desc') {
      return true;
    } else if (this._targetSorting === 'none') {
      return false;
    } else {
      return this._targetSorting as BciPicklistCompare;
    }
  }

  // TODO Restore: Replace this input with _targetSorting
  @Input() set targetSorting(value: boolean | BciPicklistCompare) {
    if (typeof value === 'boolean') {
      if (value) {
        this._targetSorting = 'asc';
      } else {
        this._targetSorting = 'none';
      }
    } else {
      this._targetSorting = value;
    }
  }

  /**
   * Sort behavior of the target list. If a comparison function is specified, it is evaluated. If using a comparison function return
   * -1 if the first element should appear below the second element, 0 if both elements are equal, or 1 if the second element should
   * appear below the first element.
   */
  _targetSorting: 'none' | 'asc' | 'desc' | 'manual' | BciPicklistCompare = 'asc'; // TODO Restore: Use 'none' as default

  /** Whether manual sorting of the target list is enabled. */
  get manualTargetSorting(): boolean {
    return typeof this.targetSorting === 'boolean' && this.targetSorting === false; // TODO Restore: return this._targetSorting === 'manual';
  }

  /** Whether self dropping on target must be disabled. */
  get dropSelfTargetDisabled(): boolean {
    return this._targetSorting && this.picklistService.isTargetFilterActive;
  }

  /** Propagates a copy of the current source list on change. */
  @Output() sourceChange: EventEmitter<any[]> = new EventEmitter();

  /** Propagates a copy of the current target list on change. */
  @Output() targetChange: EventEmitter<any[]> = new EventEmitter();

  /** Propagates a copy of the current source selected list on change. */
  @Output() sourceSelectedChange: EventEmitter<any[]> = new EventEmitter();

  /** Propagates a copy of the current target selected list on change. */
  @Output() targetSelectedChange: EventEmitter<any[]> = new EventEmitter();

  /** Used for component-wide unsubscription on destroy. */
  private ngUnsubscribe = new Subject<void>();

  /** Used for unsubscribing from key up events coming form the source filter text box. */
  private sourceFilterKeyUpUnsubscribe = new Subject<void>();

  /** Used for unsubscribing from key up events coming form the target filter text box. */
  private targetFilterKeyUpUnsubscribe = new Subject<void>();

  private _source: any[] = [];
  private _target: any[] = [];

  private _sourceSelected: any[] = [];
  private _targetSelected: any[] = [];

  /** Number of currently available source items. */
  private sourceItemCount: number;

  /** Number of currently available target items. */
  private targetItemCount: number;

  constructor(
    public picklistIntl: BciPicklistIntl,
    private picklistService: PicklistService,
    private changeDetector: ChangeDetectorRef
  ) {
  }

  ngOnInit(): void {
    this.picklistService.initialize(
      this.source,
      this.target,
      this._sourceSorting,
      this._targetSorting,
      this.displayProperty,
      this.mode
    );
    this.initSourceFilter(this.sourceFilter);
    this.initTargetFilter(this.targetFilter);

    // Subscribe after filters have been applied
    this.subscribeToPicklistService();

    this.picklistIntl.changes.pipe(takeUntil(this.ngUnsubscribe)).subscribe(() => this.changeDetector.detectChanges());
  }

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

  /** Move selected source items to target list. */
  onAddSelected(): void {
    this.picklistService.addSelection();
  }

  /** Move all source items to target list. */
  onAddAll(): void {
    this.picklistService.addAll();
  }

  /** Move selected target items to source list. */
  onRemoveSelected(): void {
    this.picklistService.removeSelection();
  }

  /** Move all target items to source list. */
  onRemoveAll(): void {
    this.picklistService.removeAll();
  }

  /** True if no source items available or current count of source items including current count of target items reached target items max limit if set*/
  isAddAllButtonDisabled(): boolean {
    return !this.isSourceItemsAvailable || this.sourceItemCount + this.targetItemCount > this._targetItemsMaxLimit;
  }

  /** Check all source items. */
  onSelectAll(responsibility: BciPicklistResponsibility): void {
    switch (responsibility) {
      case 'source': {
        this.picklistService.selectAllSourceItems();
        break;
      }
      case 'target': {
        this.picklistService.selectAllTargetItems();
        break;
      }
    }
  }

  /** Uncheck all source items. */
  onDeselectAll(responsibility: BciPicklistResponsibility): void {
    switch (responsibility) {
      case 'source': {
        this.picklistService.deselectAllSourceItems();
        break;
      }
      case 'target': {
        this.picklistService.deselectAllTargetItems();
        break;
      }
    }
  }

  /**
   * Initializes the source filter.
   * @param sourceFilter Filter is disabled when set to false.
   */
  private initSourceFilter(
    sourceFilter: boolean | string | BciPicklistStringFilter | Observable<BciPicklistFilter>
  ): void {
    if (typeof sourceFilter === 'boolean') {
      if (sourceFilter) {
        this.picklistService.setSourceFilter('');
        this.sourceFilterKeyUpUnsubscribe.next();
        this.sourceFilterKeyup
          .pipe(takeUntil(this.ngUnsubscribe), takeUntil(this.sourceFilterKeyUpUnsubscribe))
          .subscribe((filter) => this.picklistService.setSourceFilter(filter));
      } else {
        this.picklistService.setSourceFilter(false);
      }
    } else if (typeof sourceFilter === 'string') {
      this.picklistService.setSourceFilter(sourceFilter);
      this.sourceFilterKeyUpUnsubscribe.next();
      this.sourceFilterKeyup
        .pipe(takeUntil(this.ngUnsubscribe), takeUntil(this.sourceFilterKeyUpUnsubscribe))
        .subscribe((filter) => this.picklistService.setSourceFilter(filter));
    } else if (sourceFilter instanceof Observable) {
      this.picklistService.setSourceFilter(sourceFilter);
    } else {
      this.sourceFilterKeyUpUnsubscribe.next();
      this.sourceFilterKeyup
        .pipe(takeUntil(this.ngUnsubscribe), takeUntil(this.sourceFilterKeyUpUnsubscribe))
        .subscribe((filter) =>
          this.picklistService.setSourceFilter((item: any): boolean => {
            return sourceFilter(item, filter);
          })
        );
    }
  }

  /**
   * Initializes the target filter.
   * @param targetFilter Filter is disabled when set to false.
   */
  private initTargetFilter(
    targetFilter: boolean | string | BciPicklistStringFilter | Observable<BciPicklistFilter>
  ): void {
    if (typeof targetFilter === 'boolean') {
      if (targetFilter) {
        this.picklistService.setTargetFilter('');
        this.targetFilterKeyUpUnsubscribe.next();
        this.targetFilterKeyup
          .pipe(takeUntil(this.ngUnsubscribe), takeUntil(this.targetFilterKeyUpUnsubscribe))
          .subscribe((filter) => this.picklistService.setTargetFilter(filter));
      } else {
        this.picklistService.setTargetFilter(false);
      }
    } else if (typeof targetFilter === 'string') {
      this.picklistService.setTargetFilter(targetFilter);
      this.targetFilterKeyUpUnsubscribe.next();
      this.targetFilterKeyup
        .pipe(takeUntil(this.ngUnsubscribe), takeUntil(this.targetFilterKeyUpUnsubscribe))
        .subscribe((filter) => this.picklistService.setTargetFilter(filter));
    } else if (targetFilter instanceof Observable) {
      this.picklistService.setTargetFilter(targetFilter);
    } else {
      this.targetFilterKeyUpUnsubscribe.next();
      this.targetFilterKeyup
        .pipe(takeUntil(this.ngUnsubscribe), takeUntil(this.targetFilterKeyUpUnsubscribe))
        .subscribe((filter) =>
          this.picklistService.setTargetFilter((item: any): boolean => {
            return targetFilter(item, filter);
          })
        );
    }
  }

  /** Subscribes filtered and sorted source and target lists. */
  private subscribeToPicklistService(): void {
    this.picklistService.processedSourceItems.pipe(takeUntil(this.ngUnsubscribe)).subscribe((items) => {
      this.sourceItemCount = items.length;
      this.isSourceItemsAvailable = items && items.length > 0;
    });

    this.picklistService.processedTargetItems.pipe(takeUntil(this.ngUnsubscribe)).subscribe((items) => {
      this.targetItemCount = items.length;
      this.isTargetItemsAvailable = items && items.length > 0;
    });

    this.picklistService.selectionState$.pipe(takeUntil(this.ngUnsubscribe)).subscribe((state) => {
      this.isSomeSourceItemSelected = state.sourceItemsSelected;
      this.isSomeTargetItemSelected = state.targetItemsSelected;
    });

    this.picklistService.sourceItemsChange.pipe(takeUntil(this.ngUnsubscribe)).subscribe((items) => {
      this.sourceChange.emit(items);
    });
    this.picklistService.targetItemsChange.pipe(takeUntil(this.ngUnsubscribe)).subscribe((items) => {
      this.targetChange.emit(items);
    });

    this.picklistService.sourceSelectedItemsChange.pipe(takeUntil(this.ngUnsubscribe)).subscribe((items) => {
      this.sourceSelectedChange.emit(items);
    });

    this.picklistService.targetSelectedItemsChange.pipe(takeUntil(this.ngUnsubscribe)).subscribe((items) => {
      this.targetSelectedChange.emit(items);
    });

    combineLatest([this.picklistService.processedTargetItems, this.picklistService.sourceSelectedItemsChange]).pipe(takeUntil(this.ngUnsubscribe))
      .subscribe(([targetItems, sourceSelection])=> {
        this.wouldSourceSelectionExceedTargetItemsMaxLimit = this._targetItemsMaxLimit < targetItems.length + sourceSelection.length;
      });
  }
}
