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

import { Injectable, OnDestroy } from '@angular/core';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { debounceTime, takeUntil } from 'rxjs/operators';

import { BciCorePicklistItem, CoreSelectionState } from './bci-core-picklist.model';
import { BciCorePicklistMode, BciCorePicklistFilter, BciCorePicklistCompare, BciCorePicklistResponsibility } from './bci-core-picklist.types';


/**
 * Enables communication between the lists of available and selected items.
 */
@Injectable()
export class BciCorePicklistService implements OnDestroy {
  /** Filtered and sorted source list items. */
  processedSourceItems: Observable<BciCorePicklistItem[]>;

  /** Filtered and sorted target list items. */
  processedTargetItems: Observable<BciCorePicklistItem[]>;

  /** Passes the current source list on if it has been changed. */
  sourceItemsChange: Subject<any[]>;

  /** Passes the current target list on if it has been changed. */
  targetItemsChange: Subject<any[]>;

  /** Passes the current number of source/target items that has been changed. */
  noOfItemsChanged: Subject<any>;

  /** Selection state of available and selected source items. */
  CoreSelectionState$: Observable<CoreSelectionState>;

  /** Passes the current source selected list on if it has been changed. */
  sourceSelectedItemsChange: Subject<any[]>;

  /** Passes the current source selected list on if it has been changed. */
  targetSelectedItemsChange: Subject<any[]>;

  /** Wheter the target items max limit is reached state. */
  isTargetItemsMaxLimitReachedState: Observable<boolean>;

  /** Property that is displayed in the source and target list. */
  get displayProperty(): any {
    return this._displayProperty;
  }

  set displayProperty(value: any) {
    if (
      this.sourceItems &&
      this.sourceItems.length > 0 &&
      !this.checkDisplayProperty(
        this.sourceItems.map((elem) => elem.value),
        value
      )
    ) {
      throw Error('Display property not in list of source objects.');
    }
    if (
      this.targetItems &&
      this.targetItems.length > 0 &&
      !this.checkDisplayProperty(
        this.targetItems.map((elem) => elem.value),
        value
      )
    ) {
      throw Error('Display property not in list of target objects.');
    }
    this._displayProperty = value;
  }

  /**
   * How the picklist behaves. In move mode items are moved from source to target and vice versa.
   * In copy mode copies of the original source items are moved to target list.
   */
  mode: BciCorePicklistMode;

  /** Current source filter. */
  private sourceFilter: false | string | BciCorePicklistFilter;

  /** Current target filter. */
  private targetFilter: false | string | BciCorePicklistFilter;

  /**
   * 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.
   */
  private sourceSortBehavior: 'none' | 'asc' | 'desc' | BciCorePicklistCompare;

  /**
   * 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.
   */
  private targetSortBehavior: 'none' | 'asc' | 'desc' | 'manual' | BciCorePicklistCompare;

  /** Time interval in milliseconds used as debounce time for filters. */
  private readonly filterInputDebounceTime = 300;
  private CoreSelectionStateSubject: BehaviorSubject<CoreSelectionState>;

  /** Current list of source items. */
  private sourceItems: BciCorePicklistItem[] = [];

  /** Max limit of target items. */
  private targetItemsMaxLimit: number;

  /** Current list of target items. */
  private targetItems: BciCorePicklistItem[] = [];

  private readonly processedSourceItemsSubject: BehaviorSubject<BciCorePicklistItem[]>;
  private readonly processedTargetItemsSubject: BehaviorSubject<BciCorePicklistItem[]>;
  private readonly processedIsTargetItemsLimitReachedSubject: BehaviorSubject<boolean>;

  private sourceFilterSubject: Subject<string | BciCorePicklistFilter>;
  private targetFilterSubject: Subject<string | BciCorePicklistFilter>;
  private ngUnsubscribe = new Subject<void>();
  private sourceFilterUnsubscribe = new Subject<void>();
  private targetFilterUnsubscribe = new Subject<void>();
  private _displayProperty: any;

  constructor() {
    this.processedSourceItemsSubject = new BehaviorSubject<BciCorePicklistItem[]>([]);
    this.processedTargetItemsSubject = new BehaviorSubject<BciCorePicklistItem[]>([]);
    this.processedIsTargetItemsLimitReachedSubject = new BehaviorSubject<boolean>(false);

    this.CoreSelectionStateSubject = new BehaviorSubject<CoreSelectionState>({
      sourceItemsSelected: false,
      targetItemsSelected: false,
    });

    this.sourceItemsChange = new Subject<any[]>();
    this.targetItemsChange = new Subject<any[]>();

    this.noOfItemsChanged = new Subject<any>();

    this.sourceSelectedItemsChange = new Subject<any[]>();
    this.targetSelectedItemsChange = new Subject<any[]>();

    this.isTargetItemsMaxLimitReachedState = this.processedIsTargetItemsLimitReachedSubject.asObservable();

    this.sourceFilterSubject = new Subject<string | BciCorePicklistFilter>();
    this.targetFilterSubject = new Subject<string | BciCorePicklistFilter>();

    this.processedSourceItems = this.processedSourceItemsSubject.asObservable();
    this.processedTargetItems = this.processedTargetItemsSubject.asObservable();
    this.CoreSelectionState$ = this.CoreSelectionStateSubject.asObservable();

    this.sourceFilterSubject
      .asObservable()
      .pipe(takeUntil(this.ngUnsubscribe), debounceTime(this.filterInputDebounceTime))
      .subscribe((filter) => {
        this.sourceFilter = filter;
        this.updateProcessedSources('source');
      });

    this.targetFilterSubject
      .asObservable()
      .pipe(takeUntil(this.ngUnsubscribe), debounceTime(this.filterInputDebounceTime))
      .subscribe((filter) => {
        this.targetFilter = filter;
        this.updateProcessedSources('target');
      });
  }

  ngOnDestroy(): void {
    this.sourceFilterUnsubscribe.next();
    this.sourceFilterUnsubscribe.complete();
    this.targetFilterUnsubscribe.next();
    this.targetFilterUnsubscribe.complete();

    this.ngUnsubscribe.next();
    this.ngUnsubscribe.complete();
  }

  /**
   * Initializes the service.
   * @param source List of available items.
   * @param target List of selected items
   * @param sourceSortBehavior
   * @param targetSortBehavior
   * @param displayProperty Property to display.
   * @param mode
   */
  initialize<T extends object, K extends keyof T>(
    source: T[],
    target: T[],
    sourceSortBehavior: 'none' | 'asc' | 'desc' | BciCorePicklistCompare,
    targetSortBehavior: 'none' | 'asc' | 'desc' | 'manual' | BciCorePicklistCompare,
    displayProperty?: K,
    mode: BciCorePicklistMode = 'move'
  ): void {
    this.displayProperty = displayProperty;
    this.sourceSortBehavior = sourceSortBehavior;
    this.targetSortBehavior = targetSortBehavior;
    this.mode = mode;
    this.setSourceItems(source, false);
    this.setTargetItems(target, false);
    this.updateTargetItemsMaxLimitState();
    this.updateProcessedSources();
    this.updateCoreSelectionState();
  }

  /**
   * Sets the source items.
   * @param source
   * @param updateSources
   */
  setSourceItems<T extends object>(source: T[], updateSources = true): void {
    if (!this.checkDisplayProperty(source, this.displayProperty)) {
      throw Error('Display property not in list of source objects.');
    }
    this.sourceItems.length = 0;
    this.sourceItems = this.mapToPicklistItem(source);
    this.updateProcessedSources('source');
    if (updateSources) {
      this.updateSources('source');
    }
  }

  /**
   * Sets the max limit for target items
   * @param value
   */
  setTargetItemsMaxLimit(value: number): void {
    this.targetItemsMaxLimit = value;
  }

  /**
   * Sets the target items.
   * @param target
   * @param updateSources
   */
  setTargetItems<T extends object>(target: T[], updateSources = true): void {
    if (!this.checkDisplayProperty(target, this.displayProperty)) {
      throw Error('Display property not in list of target objects.');
    }
    if (this.targetItemsMaxLimit < target.length) {
      throw Error('Target items exceeding max limit of ' + this.targetItemsMaxLimit)
    }
    this.targetItems.length = 0;
    this.targetItems = this.mapToPicklistItem(target);
    this.updateProcessedSources('target');
    if (updateSources) {
      this.updateSources('target');
    }
  }

  /**
   * Updates the source selection state.
   * @param flag If true skip source list check for selected items and pass value directly to subscribers.
   */
  updateSourceCoreSelectionState(flag: boolean): void {
    this.updateCoreSelectionState(flag, false);
  }

  /**
   * Updates the source selection.
   */
  updateSourceSelection() {
    const selected = this.getSelectedSourceItems();
    this.sourceSelectedItemsChange.next(selected);
    this.updateSourceCoreSelectionState((selected || []).length > 0);
  }

  /**
   * Updates the target selection state.
   * @param flag If true skip source list check for selected items and pass value directly to subscribers.
   */
  updateTargetCoreSelectionState(flag: boolean): void {
    this.updateCoreSelectionState(false, flag);
  }

  /**
   * Updates the target selection.
   */
  updateTargetSelection() {
    const selected = this.getSelectedTargetItems();
    this.targetSelectedItemsChange.next(selected);
    this.updateTargetCoreSelectionState((selected || []).length > 0);
  }

  /**
   * Adds all selected source items, regardless of the current filtering, to the target list. The items
   * are unselected after they've been added.
   */
  addSelection(): void {
    let itemsChanged = false;
    switch (this.mode) {
      case 'copy': {
        itemsChanged = this.copy( 'add',this.sourceItems, this.targetItems, false, true, this.sourceSortBehavior);
        break;
      }
      case 'move': {
        itemsChanged = this.move( 'add',this.sourceItems, this.targetItems, true, false, true, this.sourceSortBehavior);
        break;
      }
    }
    if (itemsChanged) {
      this.updateTargetItemsMaxLimitState();
      this.updateProcessedSources();
      this.deselectAllSourceItems();
      this.updateCoreSelectionState();
      this.updateSources();
    }
  }

  /**
   * Removes all selected target items, regardless of the current filtering, from the target list. The items
   * are unselected after they've been removed.
   */
  removeSelection(): void {
    let itemsChanged = false;
    switch (this.mode) {
      case 'copy': {
        itemsChanged = this.delete( 'remove',this.targetItems, this.targetFilter, true);
        break;
      }
      case 'move': {
        itemsChanged = this.move( 'remove', this.targetItems, this.sourceItems, false, this.targetFilter, true);
        break;
      }
    }

    if (itemsChanged) {
      this.updateTargetItemsMaxLimitState();
      this.updateProcessedSources();
      this.updateCoreSelectionState();
      this.updateSources();
    }
  }

  /**
   * Adds a list of source items to the target list. The items are unselected after they've been added.
   *
   * If target items max limit is set and reached, items will not be added.
   * @param items
   */
  addItems(...items: BciCorePicklistItem[]): void {
    if (!this.targetItemsMaxLimit || this.targetItemsMaxLimit >= items.length + this.targetItems.length) {
      items.forEach((item) => {
        const itemIndex = this.sourceItems.indexOf(item);
        if (itemIndex >= 0) {
          item.selected = false;
          switch (this.mode) {
            case 'move': {
              this.sourceItems.splice(itemIndex, 1);
              this.targetItems.push(item);
              break;
            }
            case 'copy': {
              this.targetItems.push(this.createCopy(item));
              break;
            }
          }
          this.noOfItemsChanged.next({length:items.length, action: 'add'});
          this.updateTargetItemsMaxLimitState();
          this.updateProcessedSources();
          this.updateCoreSelectionState();
          this.updateSources();
        }
      });
    }
  }

  /**
   * Removes a list of target item from the target list.
   * @param index
   * @param items
   */
  removeItems(index: number, ...items: BciCorePicklistItem[]): void {
    items.forEach((item) => {
      const itemIndex = this.targetItems.indexOf(item);
      if (itemIndex >= 0) {
        switch (this.mode) {
          case 'move': {
            item.selected = false;
            this.targetItems.splice(itemIndex, 1);
            this.sourceItems.splice(index, 0, item);
            break;
          }
          case 'copy': {
            this.targetItems.splice(itemIndex, 1);
            break;
          }
        }        
        this.noOfItemsChanged.next({length:items.length, action: 'remove'});
        this.updateTargetItemsMaxLimitState();
        this.updateProcessedSources();
        this.updateCoreSelectionState();
        this.updateSources();
      }
    });
  }

  /**
   * Inserts the items in the target list at the specified index. The index is constrained
   * by the number of target items and zero. The items are unselected after they've been inserted.
   *
   * If target items max limit is set and reached, items will not be inserted.
   * @param index
   * @param items
   */
  insertItems(index: number, ...items: BciCorePicklistItem[]): void {
    if (!this.targetItemsMaxLimit || this.targetItemsMaxLimit >= items.length + this.targetItems.length) {
      // Add one more item to the list of available target items as this is the drop placeholder item
      index = Math.max(0, Math.min(index, this.targetItems.length));

      let movingIndex = index;
      items.forEach((item) => {
        const itemIndex = this.sourceItems.indexOf(item);
        if (itemIndex >= 0) {
          item.selected = false;
          switch (this.mode) {
            case 'move': {
              this.sourceItems.splice(itemIndex, 1);
              this.targetItems.splice(movingIndex, 0, item);
              break;
            }
            case 'copy': {
              this.targetItems.splice(movingIndex, 0, this.createCopy(item));
              break;
            }
          }
          movingIndex++;
        }
      });
      this.updateTargetItemsMaxLimitState();
      this.updateProcessedSources();
      this.updateCoreSelectionState();
      this.updateSources();
    }
  }

  /**
   * Repositions the items at the given index in the target list. The index is constrained
   * by the number of target items and zero. Selection state is not changed after repositing
   * the items.
   * @param index
   * @param items
   */
  repositionTargetItems(index: number, ...items: BciCorePicklistItem[]): void {
    // Add one more item to the list of available target items as this is the drop placeholder item
    index = Math.max(0, Math.min(index, this.targetItems.length));

    // Get number of selected items whose index is before the given index
    let indexDiff = 0;
    items.forEach((item) => {
      const itemIndex = this.targetItems.indexOf(item);
      if (itemIndex < index - indexDiff) {
        indexDiff++;
      }
      this.targetItems.splice(itemIndex, 1);
      this.targetItems.push(item);
    });

    let movingIndex = index - indexDiff;
    items.forEach((item) => {
      const itemIndex = this.targetItems.indexOf(item);
      if (itemIndex >= 0) {
        this.targetItems.splice(itemIndex, 1);
        this.targetItems.splice(movingIndex, 0, item);
        movingIndex++;
      }
    });

    this.updateProcessedSources();
    this.updateCoreSelectionState();
    this.updateSources();
  }

  /** Adds all filtered source items to the target. Items are unselected after they've been added. */
  addAll(): void {
    let itemsChanged = false;
    switch (this.mode) {
      case 'copy': {
        itemsChanged = this.copy( 'add', this.sourceItems, this.targetItems, this.sourceFilter, false, this.sourceSortBehavior);
        break;
      }
      case 'move': {
        itemsChanged = this.move( 'add', this.sourceItems, this.targetItems, true, this.sourceFilter, false, this.sourceSortBehavior);
        break;
      }
    }

    if (itemsChanged) {
      this.updateTargetItemsMaxLimitState();
      this.updateProcessedSources();
      this.updateCoreSelectionState();
      this.updateSources();
      this.deselectAllSourceItems();
    }
  }

  /** Removes all filtered target items from the target list. Items are unselected after they've been removed. */
  removeAll(): void {
    let itemsChanged = false;
    switch (this.mode) {
      case 'copy': {
        itemsChanged = this.delete( 'remove', this.targetItems, this.targetFilter);
        break;
      }
      case 'move': {
        itemsChanged = this.move( 'remove', this.targetItems, this.sourceItems, false, this.targetFilter);
        break;
      }
    }

    if (itemsChanged) {
      this.updateTargetItemsMaxLimitState();
      this.updateProcessedSources();
      this.updateCoreSelectionState();
      this.updateSources();
    }
  }

  /** Selects all filtered source items. */
  selectAllSourceItems(): void {
    if (this.processedSourceItemsSubject) {
      this.processedSourceItemsSubject.value.forEach((elem) => (elem.selected = true));
      this.updateSourceSelection();
    }
  }

  /** Gets selected source items. */
  getSelectedSourceItems(): any[] {
    if (this.processedSourceItemsSubject) {
      return this.selection(this.processedSourceItemsSubject.value, this.sourceFilter, true).map((item) => item.value);
    }
    return [];
  }

  /** Selects given source items. */
  selectSourceItems<T>(items: T[]): void {
    if (this.processedSourceItemsSubject && items && items.length) {
      this.processedSourceItemsSubject.value
        .filter((elem) => items.indexOf(elem.value) !== -1)
        .forEach((elem) => (elem.selected = true));
      this.updateSourceCoreSelectionState(true);
    }
  }

  /** Selects all filtered target items. */
  selectAllTargetItems(): void {
    if (this.processedTargetItemsSubject) {
      this.processedTargetItemsSubject.value.forEach((elem) => (elem.selected = true));
      this.updateTargetSelection();
    }
  }

  /** Gets selected target items. */
  getSelectedTargetItems(): any[] {
    if (this.processedTargetItemsSubject) {
      return this.selection(this.processedTargetItemsSubject.value, this.targetFilter, true).map((item) => item.value);
    }
    return [];
  }

  /** Selects given target items. */
  selectTargetItems<T>(items: T[]): void {
    if (this.processedTargetItemsSubject && items && items.length) {
      this.processedTargetItemsSubject.value
        .filter((elem) => items.indexOf(elem.value) !== -1)
        .forEach((elem) => (elem.selected = true));
      this.updateTargetCoreSelectionState(true);
    }
  }

  /** Deselects all filtered source items. */
  deselectAllSourceItems(): void {
    if (this.processedSourceItemsSubject) {
      this.processedSourceItemsSubject.value.forEach((elem) => (elem.selected = false));
      this.updateSourceSelection();
    }
  }

  /** Deselects all filtered target items. */
  deselectAllTargetItems(): void {
    if (this.processedTargetItemsSubject) {
      this.processedTargetItemsSubject.value.forEach((elem) => (elem.selected = false));
      this.updateTargetSelection();
    }
  }

  /**
   * Applies a source filter.
   * @param filter
   */
  setSourceFilter(filter: false | string | BciCorePicklistFilter | Observable<BciCorePicklistFilter>): void {
    if (this.sourceFilter instanceof Observable) {
      this.sourceFilterUnsubscribe.next();
    }

    if (typeof filter === 'boolean') {
      this.sourceFilter = false;
      this.updateProcessedSources('source');
    } else if (typeof filter === 'string') {
      this.sourceFilterSubject.next(filter);
    } else if (filter instanceof Observable) {
      filter.pipe(takeUntil(this.sourceFilterUnsubscribe), takeUntil(this.ngUnsubscribe)).subscribe((elem) => {
        this.sourceFilterSubject.next(elem);
      });
    } else {
      this.sourceFilterSubject.next(filter);
    }
  }

  /**
   * Applies a target filter.
   * @param filter
   */
  setTargetFilter(filter: false | string | BciCorePicklistFilter | Observable<BciCorePicklistFilter>): void {
    if (this.targetFilter instanceof Observable) {
      this.targetFilterUnsubscribe.next();
    }

    if (typeof filter === 'boolean') {
      this.targetFilter = false;
      this.updateProcessedSources('target');
    } else if (typeof filter === 'string') {
      this.targetFilterSubject.next(filter);
    } else if (filter instanceof Observable) {
      filter.pipe(takeUntil(this.targetFilterUnsubscribe), takeUntil(this.ngUnsubscribe)).subscribe((elem) => {
        this.targetFilterSubject.next(elem);
      });
    } else {
      this.targetFilterSubject.next(filter);
    }
  }

   /** when source sort is changed */
  setSourceSortBehavior(value) {
    this.sourceSortBehavior = value;
    this.updateProcessedSources();
  }

   /** when target sort is changed */
  setTargetSortBehavior(value) {
    this.targetSortBehavior = value;
    this.updateProcessedSources();
  }

  /** Whether a source filter is active. */
  isSourceFilterActive(): boolean {
    return this.sourceItems.length > 0 ;
  }

  /** Whether a target filter is active. */
  isTargetFilterActive(): boolean {
    return this.targetItems.length > 0;
  }

  /** get original list of source items without filter */
  getOriginalSourceItems() {
    return this.sourceItems;
  }

/** get original list of target items without filter */
  getOriginalTargetItems() {
    return this.targetItems
  }

  /**
   * Moves all selected and/or filtered elements from source to target.
   *
   * On operation toTargetList is set to true: If target items max limit is set and exceeds, items will not be moved.
   * @param src
   * @param target
   * @param toTargetList
   * @param filter
   * @param onlySelected
   * @param sortBehavior
   * @return
   */
  private move(    
    action: string,
    src: BciCorePicklistItem[],
    target: BciCorePicklistItem[],
    toTargetList: boolean,    
    filter?: false | string | BciCorePicklistFilter,    
    onlySelected = false,
    sortBehavior: 'none' | 'asc' | 'desc' | BciCorePicklistCompare = 'none'
  ): boolean {
    const selection = this.selection(src, filter, onlySelected);
    if (toTargetList && selection.length + target.length > this.targetItemsMaxLimit) {
      return false;
    }
    selection.forEach((elem) => (elem.selected = false));
    this.sortList(selection, sortBehavior);
    target.push(...selection);
    selection.forEach((elem) => src.splice(src.indexOf(elem), 1));
    this.noOfItemsChanged.next({length:selection.length, action: action});
    return selection.length > 0;
  }

  /**
   * Copies the filtered items from source to target.
   *
   * If target items max limit is set and exceeds, items will not be copied.
   * @param src
   * @param target
   * @param filter
   * @param onlySelected
   * @param sortBehavior
   * @return
   */
  private copy(
    action: string,
    src: BciCorePicklistItem[],
    target: BciCorePicklistItem[],
    filter?: false | string | BciCorePicklistFilter,
    onlySelected = false,
    sortBehavior: 'none' | 'asc' | 'desc' | BciCorePicklistCompare = 'none'
  ): boolean {
    const selection = this.selection(src, filter, onlySelected);
    if (selection.length + target.length > this.targetItemsMaxLimit) {
      return false;
    }
    this.sortList(selection, sortBehavior);
    selection.forEach((elem) => {
      target.push(this.createCopy(elem, false));
    });
    
    this.noOfItemsChanged.next({length:selection.length, action: action});
    return selection.length > 0;
  }

  /**
   * Deletes all filtered items.
   * @param src
   * @param filter
   * @param onlySelected
   * @return
   */
  private delete(    
    action: string,
    src: BciCorePicklistItem[],
    filter?: false | string | BciCorePicklistFilter,
    onlySelected = false
  ): boolean {
    const selection = this.selection(src, filter, onlySelected);
    selection.forEach((elem) => src.splice(src.indexOf(elem), 1));
    this.noOfItemsChanged.next({length:selection.length, action: action});
    return selection.length > 0;
  }

  /**
   * Returns a copy of the current selection.
   * @param src
   * @param filter
   * @param onlySelected
   * @return
   */
  private selection(
    src: BciCorePicklistItem[],
    filter?: false | string | BciCorePicklistFilter,
    onlySelected = false
  ): BciCorePicklistItem[] {
    let selection: Array<any>;
    if (typeof filter === 'boolean') {
      selection = onlySelected ? src.filter((elem) => elem.selected) : [...src];
    } else {
      selection = this.filter(onlySelected ? src.filter((elem) => elem.selected) : src, filter);
    }
    return selection;
  }

  /**
   * Map input data to a list of picklist items. If no display property is available
   * but the input data is an array of objects the JSON representation is used as label.
   * @param src Input data.
   */
  private mapToPicklistItem<T>(src: T[]): BciCorePicklistItem[] {
    const dest: BciCorePicklistItem[] = [];
    if (src.length) {
      if (this.displayProperty) {
        src.forEach((elem) => {
          dest.push({
            selected: false,
            label: elem[this.displayProperty],
            value: elem,
            dragging: false,
          });
        });
      } else {
        src.forEach((elem) => {
          dest.push({
            selected: false,
            label:
              typeof elem === 'number' || typeof elem === 'string' || typeof elem === 'boolean'
                ? elem.toString()
                : JSON.stringify(elem),
            value: elem,
            dragging: false,
          });
        });
      }
    }
    return dest;
  }

  /**
   * Check if target items max limit is set and pass the current target items max limit reached check state to the subscribes.
   * True if current items in target list are greater or equals target items max limit.
   */
  private updateTargetItemsMaxLimitState() {
    if (this.targetItemsMaxLimit) {
      this.processedIsTargetItemsLimitReachedSubject.next(this.targetItems.length >= this.targetItemsMaxLimit);
    }
  }

  /**
   * Pass the filtered and sorted arrays of source and target items to the subscribers. No filter or sorting will be
   * applied if not available.
   * @param origin
   */
  private updateProcessedSources(origin?: BciCorePicklistResponsibility): void {
    if (origin === 'source' || !origin) {
      this.processedSourceItemsSubject.next(this.sortSource(this.filter(this.sourceItems, this.sourceFilter)));
    }
    if (origin === 'target' || !origin) {
      this.processedTargetItemsSubject.next(this.sortTarget(this.filter(this.targetItems, this.targetFilter)));
    }
  }

  /**
   * Pass a copy of the current source/target list to the subscribers.
   * @param origin
   */
  private updateSources(origin?: BciCorePicklistResponsibility): void {
    if (origin === 'source' || !origin) {
      this.sourceItemsChange.next([...this.sortSource(this.sourceItems).map((elem) => elem.value)]);
    }
    if (origin === 'target' || !origin) {
      this.targetItemsChange.next([...this.sortTarget(this.targetItems).map((elem) => elem.value)]);
    }
  }

  /**
   * Pass the current selection state to the subscribes.
   * @param sourceItemsSelected If some source items are selected
   * @param targetItemsSelected If some target items are selected
   */
  private updateCoreSelectionState(sourceItemsSelected?: boolean, targetItemsSelected?: boolean): void {
    this.CoreSelectionStateSubject.next({
      sourceItemsSelected: sourceItemsSelected
        ? sourceItemsSelected
        : this.filter(this.sourceItems, this.sourceFilter).some((elem) => elem.selected),
      targetItemsSelected: targetItemsSelected
        ? targetItemsSelected
        : this.filter(this.targetItems, this.targetFilter).some((elem) => elem.selected),
    });
  }

  /**
   * Returns a filtered copy of the source array with all elements matching the filter term.
   * @param src Source array.
   * @param filter Filter term.
   */
  private filter(src: BciCorePicklistItem[], filter?: false | string | BciCorePicklistFilter): BciCorePicklistItem[] {
    let filteredItems: BciCorePicklistItem[];
    if (filter) {
      if (typeof filter === 'string') {
        filteredItems = src.filter((elem) => elem.label.toLowerCase().indexOf(filter.toLowerCase()) !== -1);
      } else {
        filteredItems = src.filter((elem) => filter(elem.value));
      }
    } else {
      filteredItems = [...src];
    }
    return filteredItems;
  }

  /**
   * Sorts the given source items using the defined sort behavior.
   * @param items Source items.
   */
  private sortSource(items: BciCorePicklistItem[]): BciCorePicklistItem[] {
    return this.sortList(items, this.sourceSortBehavior);
  }

  /**
   * Sorts the given target items using the defined sort behavior.
   * @param items Target items.
   */
  private sortTarget(items: BciCorePicklistItem[]): BciCorePicklistItem[] {
    return this.sortList(items, this.targetSortBehavior);
  }

  private sortList(
    items: BciCorePicklistItem[],
    sortBehavior: 'none' | 'asc' | 'desc' | 'manual' | BciCorePicklistCompare
  ): BciCorePicklistItem[] {
    if (items && items.length > 1 && sortBehavior) {
      if (sortBehavior === 'asc' || sortBehavior === 'desc') {
        if (sortBehavior === 'asc') {
          return items.sort((a, b) => {
            if (a.label === b.label) {
              return 0;
            } else {
              return a.label < b.label ? -1 : 1;
            }
          });
        } else {
          return items.sort((a, b) => {
            if (a.label === b.label) {
              return 0;
            } else {
              return a.label < b.label ? 1 : -1;
            }
          });
        }
      } else if (sortBehavior === 'none' || sortBehavior === 'manual') {
        // Ignore
      } else {
        items.sort((a: BciCorePicklistItem, b: BciCorePicklistItem): number => {
          return (sortBehavior as BciCorePicklistCompare)(a.value, b.value);
        });
      }
    }
    return items;
  }

  /**
   * Whether the display property is available in the object.
   * @param elems
   * @param displayProperty
   */
  private checkDisplayProperty<T extends object, K extends keyof T>(elems: T[], displayProperty?: K): boolean {
    return !(displayProperty && elems && elems.length > 0 && !(displayProperty in elems[0]));
  }

  /**
   * Creates a new picklist item that reuses the property values of the original item.
   * @param item
   * @param selected If defined, the selection state is overwritten.
   */
  private createCopy(item: BciCorePicklistItem, selected?: boolean): BciCorePicklistItem {
    return {
      label: item.label,
      selected: selected !== undefined ? selected : item.selected,
      value: item.value,
      dragging: item.dragging,
    };
  }
}
