import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ChangeDetectorRef, ViewChild, ElementRef, OnInit, HostListener, AfterViewInit } from '@angular/core';
import { FancySettings } from './fancy-settings.model';
import { StringUtils } from '../../utilities/string-utils';
import { LinqUtils } from '../../utilities/linq-utils';
import { FancyDragDrop, FancyEditorInstance, FancySelection } from './fancy-selection.model';
import { first, take, takeUntil } from 'rxjs/operators';
import { UuidUtils } from '../../utilities/uuid-utils';
import { MsgBoxService } from '../../services/msg-box.service';
import { ADButtons, ADIcon } from '../alert-dlg/alert-dlg.model';
import { DebounceTimer } from '../../utilities/timer-utils';
import { CDK_DRAG_CONFIG, CdkDragDrop, CdkDragEnd, CdkDragMove, CdkDragStart, moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop';
import { FancyDisplayRow, FancyGroup, FancyColumn, FancyTag, FtTrigger, FtEditableType, } from './fancy-row-column.model';
import { Observable, of, Subject, Subscription } from 'rxjs';
import { NumberUtils } from '../../utilities/number-utils';
import { DateUtilsService } from '../../services/date-utils.service';
import { ObjectUtils } from '../../utilities/object-utils';
import { ArrayUtils } from '../../utilities/array-utils';
import { DropFileEvent } from '../../shared/file-drag-drop/file-drag-drop.directive';
import { Console } from 'console';
import { CheckBoxComponent } from '@progress/kendo-angular-inputs';
import { EasyEditorService } from '../../ui/easy-editor/easy-editor.service';
import { ContextMenuItem } from '../../directives/right-click-menu/context-menu-item.model';

const DATE_FORMAT = 'MM/dd/yyyy'
const DATE_TIME_FORMAT = 'MM/dd/yyyy hh:mm aa'
const HTML_INNERTEXT_REGEX = />((.|\n*?))<\//g;
const SCROLLING_ROW_OFFSET = -150;
const DragConfig = {
  dragStartThreshold: 0,
  pointerDirectionChangeThreshold: 5,
  zIndex: 10000
};

@Component({
  selector: 'fancy-table',
  templateUrl: './fancy-table.component.html',
  styleUrls: ['./fancy-table.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
  providers: [{ provide: CDK_DRAG_CONFIG, useValue: DragConfig }]
})
export class FancyTableComponent<T = any> implements OnInit, AfterViewInit {
  // #region Properties (57)

  private _afterReloadSubscription: Subscription;
  private _data: T[];
  private _draggingLeftColClientWidth: number;
  private _draggingLeftColIndex: number;
  private _footerData: T[];
  private _footerSettings: FancySettings;
  private _itemsPerPage: number = 10;
  private _settings: FancySettings;
  private _sortBySeq: boolean = false;
  private destroy$: Subject<void> = new Subject<void>();

  protected selectedRowIndex: number = undefined;

  @Input() public id!: string;
  @Input() public noDataMessage: string = "No Data Entered";
  @Input() public usingAltColor: boolean = false;
  @Output() public adding = new EventEmitter<FancyEditorInstance<T>>();
  @Output() public afterDisplayDataGenerated = new EventEmitter<T[]>();
  @Output() public afterReload = new EventEmitter<T[]>();
  @Output() public deleting = new EventEmitter<FancyEditorInstance<T>>();
  @Output() public doubleClick = new EventEmitter<FancySelection<T>>();
  @Output() public itemDrop = new EventEmitter<FancyDragDrop>();
  @Output() public itemDropInGroup = new EventEmitter<string>();
  @Output() public onClickOnDeleteGroup = new EventEmitter<FancyGroup<T>>();
  @Output() public onFileDropped = new EventEmitter<FancyDisplayRow<T>>();
  @Output() public onFileDroppedInGroup = new EventEmitter<FancyGroup<T>>();
  @Output() public onFilterGroupKey = new EventEmitter<FancyGroup<T>>();
  @Output() public onMouseUp = new EventEmitter<FancyDisplayRow<T>>();
  @Output() public onTagClick = new EventEmitter<{ actionTag: string, row: FancyDisplayRow<T> }>();
  @Output() public pressEnter = new EventEmitter<FancySelection<T>>();
  @Output() public rowSelected = new EventEmitter<FancySelection<T>>();
  @Output() public saving = new EventEmitter<FancyEditorInstance<T>>();
  @Output() public sortByColumn = new EventEmitter<T[]>();
  @ViewChild('checkAllBox', { static: false }) public checkAllBox: CheckBoxComponent;
  @ViewChild('table', { static: true }) public tableToMeasureElement: ElementRef;

  /**
   * All FancyDisplayRow (Not Includes Group Header)
   */
  public allRows: FancyDisplayRow[];
  public anyExpanded: boolean = true;
  //private _draggingRightColClientWidth: number;
  public colDragResizing: boolean = false;
  public displayGroup: FancyGroup<T>[];
  public draggingRow = false;
  public droppingOnGroupKey: string = null;
  /**
   * All generated displaying UI rows (Includes Group Header), the source of visibleData, if the row is visible,probably need to change the name of filteredRows
   */
  public filteredRows: FancyDisplayRow<T>[];
  public filteringDebounceTimer: DebounceTimer = new DebounceTimer(400, () => { this.generateVisibleData(); });
  public filteringGroupKey: string = null;
  public firstSelectedRow: FancyDisplayRow<T>;
  public footerDisplayData: FancyDisplayRow<T>[];
  public groupColumn: FancyColumn<T>;
  public groupValues = {};
  public groups: FancyGroup<T>[] = null;
  public lastFocusedFilterInput: HTMLInputElement;
  public maxPage: number;
  public page: number = 1;
  public pages: number[];
  public processing: boolean = false;
  //public resizingDebounceTimer: DebounceTimer = new DebounceTimer(500, () => { this.renderColMaxWidth() });
  //output focusedRow and selectedRows
  public selectedRows = new Set<FancyDisplayRow<T>>();
  public sortColumn: FancyColumn<T>;
  public tableId: string = UuidUtils.generate().substr(0, 9);
  public visibleColumns: FancyColumn<T>[];
  /**
   * Visible displaying UI rows (Includes Group Header), it's dynamic filteredRows base on scrolling
   */
  public visibleData: FancyDisplayRow[];

  // #endregion Properties (57)

  // #region Constructors (1)

  constructor(
    private dateUtilsService: DateUtilsService,
    private el: ElementRef,
    private cdRef: ChangeDetectorRef,
    private easyEditorService: EasyEditorService,
    private msgBoxService: MsgBoxService,
  ) { }

  // #endregion Constructors (1)

  // #region Public Getters And Setters (14)

  @Input() public set data(value: T[]) {
    // 
    this._data = value;
    if (this._data !== undefined) {
      this.generateDisplayData();
      //this._data.forEach((d, index) => d.index = index);
    }
    this.calculatePages(this._data);
  }

  @Input() public set footerData(value: any[]) {
    this._footerData = value;
    if (this.settings && value) {
      this.generateFooterDisplayData();
    }
  }

  // Optional.  If not set, uses regular settings
  @Input() public set footerSettings(value: FancySettings) {
    this._footerSettings = value;
  }

  @Input() public set itemsPerPage(value: number) { // 0 to disable pager
    this._itemsPerPage = Number(value);
    this.calculatePages(this._data);
  }

  @Input() public set settings(value: FancySettings) {
    this._settings = new FancySettings({ ...value });
    if (value) {
      this.initializeColumns();
      if ((value.customGroups || this.data) && !this.visibleData) {
        this.generateDisplayData();
        if (this._settings.reloadSubject) {
          this._settings.reloadSubject.pipe(takeUntil(this.destroy$)).subscribe(result => {
            this.reload();
          });
        }
        if (this._settings.reloadSettingSubject) {
          this._settings.reloadSettingSubject.pipe(takeUntil(this.destroy$)).subscribe(result => {
            this._settings = new FancySettings({ ...result });
            this.reload();
          });
        }
      }
    }

    // if (this.anyGroups) {
    //   
    //   this.grouped(this.filtered(this.data));
    // }
  }

  public get anyFiltered(): boolean {
    //null check for checkbox equals false
    return this.settings.columns && this.settings.columns.some(c => {
      if (c.filter != undefined && c.filter != '') return true;
      if (['date', 'dateTime'].includes(c.type) && c.dateRange != undefined) return true;
      return false;
    }
    );
  }

  public get anyGroups(): boolean {
    return !!this._settings.customGroups || (this._settings.columns ? this._settings.columns.some(c => c.group) : false)
  }

  public get data(): T[] {
    return this._data;
  }

  public get footerData(): any[] {
    return this._footerData;
  }

  public get footerSettings(): FancySettings {
    return this._footerSettings || this._settings;
  }

  public get hasScrollBar(): boolean {
    var div = document.querySelector(`.tableContainer${this.tableId}`);
    if (div) {
      return div.scrollHeight > div.clientHeight;
    }
    return false;
  }

  public get itemsPerPage(): number {
    return Number(this._itemsPerPage);
  }

  public get settings(): FancySettings {
    return this._settings;
  }

  public get visibleColumnCount(): number {
    if (this._settings) {
      let count = this._settings.columns.map(d => d.visible ? 1 : 0).reduce((a, b) => a + b, 0);
      if (this.anyGroups) {
        count++;
      }
      return count;
    }
    return 0;
  }

  // #endregion Public Getters And Setters (14)

  // #region Public Methods (68)

  public ngOnInit(): void {
  }

  public ngAfterViewInit() {
    // setTimeout(() => {
    //   this.renderColMaxWidth();
    // }, 200);
    console.log('Fancytable ngAfterViewInit');
  }

  public ngOnDestroy() {
    this.destroy$.next();
    this.destroy$.complete();
  }

  @HostListener('focus', ['$event'])
  public focusLastAccessFilter() {
    if (this.lastFocusedFilterInput) {
      this.lastFocusedFilterInput.focus();
    }
  }

  @HostListener('window:resize', ['$event'])
  public screenResizing() {
    //this.resizingDebounceTimer.resetTimer();
  }

  public addRow(defaultData: T = <T>{}) {
    this.openEditor(-1, defaultData);
    // this.dialogService.open(FancyTableEditorComponent, {
    //   context: {
    //     columns: this.settings.columns,
    //     datum: defaultData,
    //     isAdding: true,
    //     index: -1,
    //     validation: this.settings.addingValidation
    //   },
    // }).onClose.pipe(take(1)).subscribe(instance => {
    //   if (instance) {
    //     this.adding.emit({ index: -1, object: instance });
    //   }
    // });
  }

  public checkboxCheckAll(col: FancyColumn, checked: boolean) {
    //Ignore intermediate status
    if (checked !== null) {
      this.filteredRows.filter(r => r.visible && !r.isGroupHeader).forEach(datum => {
        this.toggleCheckBox(col, datum, checked, true);
      });
      this.reload();
    }
  }

  public checkboxGetFilter(filter) {
    return {
      undefined: 0,
      true: 1,
      false: 2,
    }[filter];
  }

  public checkboxSetFilter(colName: string, filter: number) {
    let checked = [
      undefined,
      true,
      false,
    ][filter];
    this.settings.columns.find(c => c.name === colName).filter = checked;
    this.generateVisibleData();
  }

  public clearAllFilters() {
    for (let i = 0; i < this.settings.columns.length; i++) {
      const col = this.settings.columns[i];
      col.filter = undefined;
      col.dateRange = undefined;
    }
    this.filteringGroupKey = null;
    this.reload();
  }

  public colResizeDragResize(col: FancyColumn, $event: CdkDragMove<T>) {
    let leftColumn = this.visibleColumns[this._draggingLeftColIndex];
    //let rightColumn = this.visibleColumns[this._draggingLeftColIndex + 1];
    console.log('resize', $event.distance.x);

    let offsetX = $event.distance.x;
    leftColumn.widthPct = 0;
    leftColumn.widthRem = 0;
    leftColumn.widthPx = this._draggingLeftColClientWidth + offsetX;
    this.calculateColWidth();
    // rightColumn.widthPct = 0;
    // rightColumn.widthRem = 0;
    // rightColumn.widthPx = this._draggingRightColClientWidth - offsetX;
  }

  public colResizeDragStart(col: FancyColumn, $event: CdkDragStart<T>) {
    this.colDragResizing = true;

    let colIndex = this.visibleColumns.indexOf(col);
    this._draggingLeftColIndex = colIndex;// < (this.visibleColumns.length - 1) ? colIndex : (colIndex - 1);

    this._draggingLeftColClientWidth = document.getElementById(`${this.tableId}col-${this._draggingLeftColIndex}`).clientWidth;
    //this._draggingRightColClientWidth = document.getElementById(`${this.tableId}col-${this._draggingLeftColIndex + 1}`).clientWidth;
  }

  public colResizeEnd(col: FancyColumn, $event: CdkDragEnd<T>) {
    this.colDragResizing = false;
  }

  public columnFilterFunction(column: FancyColumn): boolean {
    return (column.filter !== undefined || column.filterFunction !== undefined || column.dateRange !== undefined);
  }

  public deleteRow(selectedRowIndex: number = undefined) {
    if (undefined === selectedRowIndex) {
      selectedRowIndex = this.firstSelectedRow.index;//this.data.findIndex(d => this.selectedRows.has(d));
    }
    let _deletingIndex = selectedRowIndex;
    let deletingInstance = { index: _deletingIndex, object: this.data[_deletingIndex] };

    let msg = null;
    let async = false;
    if (this.settings.deletingValidation) {
      let tempResult = this.settings.deletingValidation(deletingInstance);
      if (typeof tempResult === 'string') {
        msg = tempResult;
      }
      else {
        async = true;
        tempResult.pipe(first()).subscribe(result => {
          this.showDeletingConfirm(result, deletingInstance);
        });
      }
    }
    if (false == async) {
      this.showDeletingConfirm(msg, deletingInstance);
    }
  }

  public detectChanges() {
    if (!this.cdRef['destroyed']) {
      this.cdRef.detectChanges();
    }
  }

  public drop(event: CdkDragDrop<T[]>) {
    console.log('dropRow', this.droppingOnGroupKey);
    this.draggingRow = false;
    if (this.droppingOnGroupKey) {
      return;
    }
    if (false == this.settings.allowDragAndDrop || event.currentIndex == event.previousIndex) return;

    //let datum = this.visibleData.find(d => d.uiSeq == event.previousIndex);
    //if (false == this.selectedRows.has(datum)) this.selectedRows.add(datum);

    //moveItemInArray(this.visibleData, event.previousIndex, event.currentIndex);
    let previousGroup: FancyGroup = null;
    let currentGroup: FancyGroup = null;

    let previousRow = undefined as FancyDisplayRow<T>;
    let nextRow = undefined as FancyDisplayRow<T>;
    let moveDown = event.currentIndex > event.previousIndex;
    //if it's not moving to the end of table, should always has next row at current index before resorting   
    if (event.currentIndex != (this.visibleData.length - 1)) {
      const displayRow = this.visibleData[event.currentIndex + (moveDown ? 1 : 0)];
      if (!displayRow.isGroupHeader) {
        nextRow = displayRow;
        console.log('nextRow', displayRow.rowDatum['description']);
      }
    }

    //if current index > 0 should always has previous row at current index-1
    if (event.currentIndex > 0) {
      const displayRow = this.visibleData[event.currentIndex - (moveDown ? 0 : 1)];
      if (!displayRow.isGroupHeader) {
        previousRow = displayRow;
        console.log('previousRow', displayRow.rowDatum['description']);
      }
    }

    var offset = 0;
    let selectedRows = Array.from(this.selectedRows)

    if (this.settings.taggedRowData)

      if (this.anyGroups) {
        let moveDown = event.previousIndex < event.currentIndex;
        currentGroup = this.groups.slice().reverse().find(g => g.seq < (event.currentIndex + (moveDown ? 1 : 0)));
      }

    for (let i = 0; i < selectedRows.length; i++) {
      const row = selectedRows[i];
      let moveTo = i == 0 ? event.currentIndex : (this.filteredRows.indexOf(selectedRows[0]) + i - 1);

      if (this.anyGroups) {
        previousGroup = this.groups.slice().reverse().find(g => g.seq < row.uiSeq);
      }

      moveItemInArray(this.filteredRows, this.filteredRows.indexOf(row), moveTo);
      if (this.anyGroups && previousGroup && currentGroup && previousGroup != currentGroup) {
        //expands group if it collapsed
        if (false == currentGroup.expanded) currentGroup.expanded = true;
        row.groupKey = currentGroup.groupKey;
        if (this.groupColumn) {
          row.rowDatum[this.groupColumn.name] = currentGroup.groupValue;
        }

        let insertIndex = 0;
        if (currentGroup.rows.length > 0) {
          insertIndex = previousRow ? Math.max(0, currentGroup.rows.indexOf(previousRow)) : currentGroup.rows.length;
        }

        transferArrayItem(previousGroup.rows, currentGroup.rows, previousGroup.rows.indexOf(row), insertIndex)
      }
    }
    this.filteredRows.forEach((row, i) => {
      row.uiSeq = i;
      row.seq = i;
    });
    //Enable sort by Seq after drag
    this._sortBySeq = true;
    this.generateVisibleData(false);
    //Set focus to drag and drop row
    this.firstSelectedRow = this.filteredRows[event.currentIndex];
    this.selectedRows = new Set([this.firstSelectedRow]);
    //this.setFocusedRowByUiSeq(event.currentIndex);
    this.itemDrop.emit({
      previousIndex: event.previousIndex, currentIndex: event.currentIndex,
      previousGroup, currentGroup, previousRow, nextRow,
      focusRow: this.firstSelectedRow,
    });
  }

  public dropColumn(event: CdkDragDrop<T[]>) {
  }

  public expandedGroups(key: any): boolean {
    let group = this.getGroupByKey(key);
    return group && group.expanded;
  }

  public fileDropped(row: FancyDisplayRow<T>, event: DropFileEvent) {
    if (row == null) {
      row = new FancyDisplayRow();
    }
    row.uiFileDropList = event.files;
    this.onFileDropped.next(row);

    console.log('FancyTable file drop', row);
    //{{this.tableId}}-GroupRow-{{row.index}}
    //{{this.tableId}}-Row-{{row.index}}
    //this.onFileDropped.next(files);
  }

  public filterGroupKey(group: FancyGroup<T>) {
    if (!this.draggingRow) {
      if (this.filteringGroupKey == group.groupKey) {
        this.filteringGroupKey = null;
      } else {
        this.filteringGroupKey = group.groupKey;
      }
      this.visibleData = [];
      this.generateVisibleData(true);
      this.onFilterGroupKey.emit(group);
    }
  }

  public filtered(data: FancyDisplayRow[]): FancyDisplayRow[] {
    // Todo: configure this somewhere in FancySettings
    let filterAll = true;

    if (data) {
      data.forEach(datum => {
        datum.visible = false;
      });

      data.filter(d => {
        let filteredColumns = this.settings.columns.filter(this.columnFilterFunction);
        if (filterAll) {
          return filteredColumns.every(c => this.rowFilterFunction(d, c));
        }
        else { // filter Any
          return filteredColumns.some(c => this.rowFilterFunction(d, c)); // unused until filterAll is configurable
        }
      }).forEach(d => d.visible = true);
    }
    return data;
  }

  public generateDisplayData() {
    this.filteredRows = [];
    this.allRows = [];
    if (this.settings && (this.data || this.settings.customGroups)) {
      //Do NOT clean up visible data here, that will cause table reload and lose dynamic data
      //this.visibleData = [];
      if (this.settings.customGroups) {
        //this.groups = null;
      } else {
        this.data.forEach((d, index) => {
          //d.index = index;
          this.allRows.push({
            seq: index,
            uiSeq: index,
            index: index,
            rowDatum: d,
            colHtmlTexts: {},//ObjectUtils.Clone(d),
            colTooltips: {},//ObjectUtils.Clone(d),
            isGroupHeader: false
          } as FancyDisplayRow);
        });

        this.visibleColumns.forEach(col => {
          if (!col.group && col.visible) {
            this.allRows.forEach(datum => {
              this.getFieldDisplayValue(datum, col);
              //datum[col.name] = col.valuePrepareFunction(datum);
            });
            col.uiWidth = this.getColWidthSetting(col);
          }
        });
      }

      this.generateVisibleData();
      this.generateFooterDisplayData();
      this.afterDisplayDataGenerated.emit();
    }
  }

  public generateFooterDisplayData() {
    this.footerDisplayData = [];
    if (this.settings && this.footerData) {
      this.footerData.forEach((d, index) => {
        this.footerDisplayData.push({
          seq: index,
          index: index,
          rowDatum: d,
          colHtmlTexts: {},
          colTooltips: {},
          isGroupHeader: false
        } as FancyDisplayRow);
      });

      this.footerSettings.columns.filter(f => f.visible).forEach(col => {
        if (!col.group) {
          this.footerDisplayData.forEach(datum => {
            this.getFieldDisplayValue(datum, col);
            //datum[col.name] = col.valuePrepareFunction(datum);
          });
        }
      });
      this.detectChanges();
    }
  }

  public generateVisibleData(reGrouping: boolean = true) {
    let visibleRowCount = this.visibleData && this.visibleData.length > this.settings.initialRows ? this.visibleData.length : this.settings.initialRows;

    if (!this.settings.customGroups) {
      this.filtered(this.allRows);
    }

    //with grouping
    if (this.anyGroups /*&& !this.groups || Object.keys(this.groups).length === 0*/) {
      this.filteredRows = [];
      let lastExpandSetting = this.groups ? this.groups.map(
        g => { return { groupKey: g.groupKey, expanded: g.expanded } }
      ) : [];
      if (!this.groups || reGrouping) {
        if (this.settings.customGroups) {
          this.groups = [];
          this.allRows = [];
          for (let i = 0; i < this.settings.customGroups.length; i++) {
            const customGroup = this.settings.customGroups[i];
            let groupKey = customGroup[this.settings.customGroupKeyFieldName];
            const rows = customGroup[this.settings.customGroupRowsFieldName].map((d, index) => {
              return {
                seq: index,
                index: index,
                rowDatum: d,
                groupKey: groupKey,
                colHtmlTexts: {},
                colTooltips: {},
                isGroupHeader: false
              } as FancyDisplayRow
            }
            ) as FancyDisplayRow<T>[];
            this.allRows = this.allRows.concat(rows);

            this.visibleColumns.forEach(col => {
              if (!col.group && col.visible) {
                rows.forEach(datum => {
                  this.getFieldDisplayValue(datum, col);
                  //datum[col.name] = col.valuePrepareFunction(datum);
                });
              }
            });
            let groupDisplayValue = customGroup[this.settings.customGroupValueFieldName];
            this.groups.push({
              seq: customGroup[this.settings.customGroupSeqFieldName],
              groupKey: groupKey,
              groupValue: this.settings.groupValuePrepareFunction ? this.settings.groupValuePrepareFunction(groupDisplayValue, rows) : groupDisplayValue,
              rows: this.filtered(rows),
              expanded: true,
              uiAllowDeleteWhenEmpty: customGroup['uiAllowDeleteWhenEmpty']
            } as FancyGroup<T>);
          }
        } else {
          this.groups = this.grouped(this.allRows.filter(d => d.visible));
        }

        if (this.settings.groupSortDirection) {
          this.groups = this.sortedGroupKeys(this.groups);
        }

        if (this.settings.customGroups) {
          //this.allRows = ArrayUtils.ConcatMultiArray(this.groups.map(g => g.rows));
          for (let i = 0; i < this.allRows.length; i++) {
            const row = this.allRows[i];
            row.index = i;
            row.uiSeq = i;
          }
        }
      }

      //Adding Group header
      this.groups.forEach(group => {
        if (!this.filteringGroupKey || this.filteringGroupKey == group.groupKey) {
          if (group.rows.filter(r => r.visible).length > 0 || this.settings.displayEmptyGroup) {
            this.filteredRows.push({ isGroupHeader: true, groupKey: group.groupKey } as FancyDisplayRow);
            group.expanded = lastExpandSetting.length == 0 || lastExpandSetting.find(s => s.groupKey == group.groupKey)?.expanded;
            if (group.expanded === undefined) {
              group.expanded = true;
            }
            //Sort grouped data
            if (group.expanded) {
              this.filteredRows = this.filteredRows.concat(this.sorted(group.rows));
            }
          }
        }
      });
    }
    else {
      //Sorting
      this.filteredRows = this.sorted(this.allRows.filter(d => d.visible));
    }

    this.visibleData = this.filteredRows.filter(r => r.isGroupHeader || r.visible).slice(0, visibleRowCount);
    this.allRows.forEach(row => row.uiSeq = -1);
    this.filteredRows.forEach((row, i) => {
      row.uiSeq = i;
      if (row.isGroupHeader) this.getGroupByKey(row.groupKey).seq = i;
    });
    this.detectChanges();
  }

  public getColWidthSetting(col: FancyColumn) {
    if (col.type == 'checkall') {
      return !col.taggingTarget ? '37px' : '55px';
    } else if (col.widthPct) {
      return col.widthPct.toString() + '%';
    } else if (col.widthRem) {
      return col.widthRem.toString() + 'rem';
    } else if (col.widthPx) {
      return col.widthPx.toString() + 'px';
    }
    return 'auto';
  }

  public getFieldDisplayValue(row: FancyDisplayRow, column: FancyColumn) {
    if (column.valuePrepareFunction) {
      let preparedValue = column.valuePrepareFunction(row.rowDatum);
      row.colHtmlTexts[column.name] = preparedValue == null ? '' : preparedValue;
    } else {
      switch (column.type) {
        case "tag":
          let tags = column.tagsPrepareFunction(row.rowDatum);
          row.colHtmlTexts[column.name] = tags;
          row.colTooltips[column.name] = StringUtils.makeCommaSeparatedString(tags.map(t => t.tooltip), true);
          break;
        case "checkall":
          //need to set all row to false by default to avoid intermediate status of the check box
          if ([undefined, null].includes(row.rowDatum[column.name])) {
            row.rowDatum[column.name] = false;
          };
          break;
        default:
          row.colHtmlTexts[column.name] = row.rowDatum[column.name];
          break;
      }
    }

    switch (column.type) {
      case "currency":
        row.colHtmlTexts[column.name] = NumberUtils.FormatCurrency(row.colHtmlTexts[column.name], "0.00");
        break;

      case "date":
        row.colHtmlTexts[column.name] = this.dateUtilsService.format(row.rowDatum[column.name], DATE_FORMAT);
        break;

      case "dateTime":
        row.colHtmlTexts[column.name] = this.dateUtilsService.format(row.rowDatum[column.name], DATE_TIME_FORMAT);
        break;
    }

    if (column.toolTipPrepareFunction) {
      row.colTooltips[column.name] = column.toolTipPrepareFunction(row.rowDatum);
    } else if (!row.colTooltips[column.name]) {
      //Parse HTML inner text for tooltip, can comment this out if got performance problem
      row.colTooltips[column.name] = StringUtils.getHtmlInnerText('' + row.colHtmlTexts[column.name]);
    }

    return row.colHtmlTexts[column.name];
  }

  public getGroupByKey(groupKey) {
    return this.groups.find(g => g.groupKey == groupKey);
  }

  public getGroupValue(key: any) {
    return this.getGroupByKey(key).groupValue;
  }

  public getScrollHeightOfIndex(index: number = -1): number {
    return 100;
  }

  public getSelected() {
    return <FancySelection<T>>{
      index: this.firstSelectedRow && this.firstSelectedRow.index >= 0 ? this.firstSelectedRow.index : -1,
      focusedRow: this.firstSelectedRow && this.firstSelectedRow.rowDatum || undefined,
      focusedCustomRowId: this.firstSelectedRow && this.firstSelectedRow.customRowId || undefined,
      selectedRows: Array.from(this.selectedRows.keys()).map(r => r?.rowDatum) as T[],
    }
  }

  public gotoPage(p: number) {
    this.page = p;
  }

  public grouped(data: FancyDisplayRow[]): FancyGroup[] {
    // only update grouping the first time, TODO: or on a change to group settings
    if (this.groupColumn /*&& !this.groups || Object.keys(this.groups).length === 0*/) {
      data.forEach(row => {
        row.groupKey = this.groupColumn.groupKeyPrepareFunction ? this.groupColumn.groupKeyPrepareFunction(row.rowDatum) : row.rowDatum[this.groupColumn.name];

      });

      let groups = ArrayUtils.GroupBy(data, r => r.groupKey);
      let tempList: FancyGroup[] = [];
      var groupCount = 0;
      for (let i = 0; i < groups.length; i++) {
        const item = groups[i];
        let group = {
          groupKey: item.key,
          seq: this.settings.groupSeqPrepareFunction ? this.settings.groupSeqPrepareFunction(item.key) : item.values[0].seq + groupCount,
          rows: item.values,
          expanded: true
        } as FancyGroup;
        group.groupValue = this.settings.groupValuePrepareFunction ? this.settings.groupValuePrepareFunction(item.key, group.rows.map(r => r.rowDatum)) : item.key;
        if (!group.groupValue) {
          //TODO:add configuration
          group.groupValue = this.settings.groupMissing;
        }
        group.rows = this.sorted(group.rows);
        tempList.push(group);
        groupCount++;
      }
      this.groups = tempList;
    }

    return this.groups;
  }

  public keyHandler(event: KeyboardEvent) {
    switch (event.key) {
      case 'ArrowDown':
        {
          let currentUiSeq = this.firstSelectedRow ? this.firstSelectedRow.uiSeq : -1;
          currentUiSeq = NumberUtils.Clamp(currentUiSeq, -1, this.visibleData.length - 2);
          this.setFocusedRowByUiSeq(currentUiSeq + 1);
        }
        event.preventDefault();
        break;

      case 'ArrowUp':
        {
          let currentUiSeq = this.firstSelectedRow ? this.firstSelectedRow.uiSeq : -1;
          currentUiSeq = NumberUtils.Clamp(currentUiSeq, 1, this.visibleData.length);
          this.setFocusedRowByUiSeq(currentUiSeq - 1);
        }
        event.preventDefault();
        break;
      case 'Enter':
        if (this.pressEnter.observers.length > 0) {
          this.pressEnter.next(this.getSelected());
          event.preventDefault();
        }
        break;
    }
  }

  public markForCheck() {
    this.cdRef.markForCheck();
  }

  public onCellMouseDown(col: FancyColumn, $event: MouseEvent) {
    if (this.settings.allowDragAndDrop && !col.draggable) {
      $event.stopPropagation();
    }
  }

  // <tbody> has this [RightClickValue]="this", so "self" is "this" FancyTableComponent
  // "this" is not in the click MouseEvent scope, afaik
  public onContextMenuOpening(self: FancyTableComponent, rightClickMenuItems: ContextMenuItem[], event: MouseEvent) {
    // datum will be undefined, since context menu is bound to tbody and unset

    let targetElement = event.target as Element;

    let targetRow = self.getTrFromChild(targetElement);

    if (self.groupColumn) {
      let targetGroup = self.getGroupKeyFromTR(targetRow) as FancyGroup<T>;
      if (targetGroup) {
        rightClickMenuItems.forEach(function (item, index) {
          item.visible = item.forGroupHeader == true;
        });
        if (self.settings.onContextMenuOpening) {
          self.settings.onContextMenuOpening(targetGroup, rightClickMenuItems, self.getSelected(), event);
        }
        return targetGroup;
      } else {
      }
    }

    let targetDatum = self.getDatumFromTR(targetRow);

    rightClickMenuItems.forEach(function (item, index) {
      item.visible = item.forGroupHeader == false;
    });
    let rowDatum = targetDatum ? targetDatum.rowDatum : null;
    self.onRowSelect(targetDatum ? targetDatum.index : -1, event);
    if (self.settings.onContextMenuOpening) {
      self.settings.onContextMenuOpening(rowDatum, rightClickMenuItems, self.getSelected(), event);
    }
    return rowDatum;
  }

  public onInfinityScrollDown() {
    this.visibleData = this.visibleData.concat(this.filteredRows.slice(this.visibleData.length, this.visibleData.length + this.settings.appendRowsOnScroll));
  }

  public onInfinityScrollUp() {
  }

  // getGroupDisplayValue(group) {
  //   let fancyGroup = this.settings && this.settings.groups.find(g => g.name === group.key);
  //   if (fancyGroup && fancyGroup.valuePrepareFunction)
  //     return fancyGroup.valuePrepareFunction(group.key);
  //   return group.key;
  // }
  public onRowMouseUp(row: FancyDisplayRow<T>, $event: MouseEvent) {
    this.onMouseUp.emit(row);
  }

  public onRowSelect(index: number, event: MouseEvent) {
    if (!this.settings.allowSelection) return;
    //let targetElement = event.srcElement as Element;
    let targetRow = this.allRows[index];
    //let datum = targetRow.rowDatum;

    if (this.selectedRows.has(targetRow) && !(event.ctrlKey || event.shiftKey)) {
      //when user click same row without control or shift key, don't need to clean out selectedRows
      return;
    }

    // 

    if (!this.settings.multiselect || !(event.ctrlKey || event.shiftKey)) {
      //if (false == (this.settings.taggedValues && this.settings.taggedValues.length > 1)) {
      //don't reset if the using the check box for row selection
      this.selectedRows = new Set();
      this.firstSelectedRow = undefined;
      //}
    }

    if (this.firstSelectedRow === undefined) {
      this.firstSelectedRow = targetRow;
      // console.log("firstSelectedRow", datum)
    } else if (this.selectedRows.has(targetRow) && this.firstSelectedRow === targetRow && this.selectedRows.size === 1) {
      this.firstSelectedRow = undefined;
    }

    // select all from `datum` to `firstSelectedRow`
    if (event.shiftKey) {
      let data = this.visibleData;
      let datumFound = false;
      let firstSelectedRowFound = false;

      for (let i = 0; i < data.length; i++) {
        if (false == data[i].isGroupHeader) {
          if (data[i] === targetRow) {
            datumFound = true;
          }
          if (data[i] === this.firstSelectedRow) {
            firstSelectedRowFound = true;
          }

          if (datumFound != firstSelectedRowFound || data[i] === targetRow || data[i] === this.firstSelectedRow) { // xor
            this.selectedRows.add(data[i]);
          } else if (this.selectedRows.has(data[i])) {
            this.selectedRows.delete(data[i]);
          }
        }
      }
    } else {
      this.selectedRows.has(targetRow) ? this.selectedRows.delete(targetRow) : this.selectedRows.add(targetRow);
    }

    this.rowSelected.emit(this.getSelected());
  }

  /**
   * If editingIndex is not supplied or undefined, use the current selected row.
   * Set editingIndex to -1 to enable Create mode.
   */
  public openEditor(editingIndex: number = undefined, defaultData: T = <T>{}) {
    if (undefined === editingIndex) {
      // selectedRowIndex = this.data.findIndex(d => this.selectedRows.has(d));
      editingIndex = this.firstSelectedRow.index;
    }

    const datum = editingIndex > -1 ? this.data[editingIndex] : defaultData; // or <T>{}?

    let onEditorClosed = this.easyEditorService.openEditorForFancyTable(datum, this.settings.columns, editingIndex);

    onEditorClosed.pipe(first()).subscribe(result => {
      if (result && result.object) {
        const instance = result.object;
        if (editingIndex > -1) {
          if (!this.saving.observed) {
            // allow for default behavior if not subscribed to saving event
            this.data[editingIndex] = instance;
            this.reload();
          } else {
            this.saving.emit({ index: editingIndex, object: instance });
          }
        } else {
          if (!this.adding.observed) {
            // allow for default behavior if not subscribed to adding event
            this.data.push(instance);
            this.reload();
          }
          else {
            this.adding.emit({ index: editingIndex, object: instance });
          }
        }
      }
    });

    return onEditorClosed;
  }

  public reload() {
    this.initializeColumns();
    this.generateDisplayData();
    // this.groups = undefined;
    this.detectChanges();
    this.afterReload.emit();
  }

  public renderColMaxWidth(): boolean {
    let divToMeasureWidth = this.el.nativeElement.clientWidth;
    if (divToMeasureWidth > 0) {
      this._settings.columns.forEach(col => {
        let maxWidth = 'auto';
        if (col.visible) {
          if (col.widthPct) {
            maxWidth = `${divToMeasureWidth * col.widthPct / 100}px`;
          } else if (col.widthRem) {
            maxWidth = col.widthRem.toString() + 'rem';
          } else if (col.widthPx) {
            maxWidth = col.widthPx.toString() + 'px';
          }
        }
        col.maxWidth = maxWidth;
      });
      this.detectChanges();
      return true;
    }
    return false;
  }

  /**
   * Will return UI seq if found it other wise return `-1`, 
   * Example:`fancyTable.renderRowHtml(s => s.id == 'The id')`
   */
  public renderRowHtml(predicate: (value: T) => boolean, focusAfterRender: boolean = false): number {
    let index = -1;
    const searchResult = this.visibleData.filter(d => d.rowDatum && predicate(d.rowDatum));
    if (searchResult) {
      for (let i = 0; i < searchResult.length; i++) {
        const row = searchResult[i];

        this.visibleColumns.forEach(col => {
          if (!col.group && col.visible) {
            this.getFieldDisplayValue(row, col);
          }
        });
        if (index == -1) {
          index = row.index;
        }
      }
    }
    if (focusAfterRender) {
      this.setFocusedRowByIndex(index);
    }
    this.detectChanges();
    return index;
  }

  public resetSort() {
    this.sortColumn = null;
    if (this.settings && this.settings.columns) {
      this.sortColumn = this.settings.columns.find(c => !StringUtils.isNullOrWhitespace(c.sortDirection));
    }
  }

  public rowFilterFunction(row: FancyDisplayRow, c: FancyColumn): boolean {
    if (row.isGroupHeader) return true;
    // Use filterFunction if it was defined
    if (c.filterFunction) {
      return c.filterFunction(row, c.filter);
    }

    // Use filter string if it was defined
    if ((c.filter !== undefined || c.dateRange !== undefined) /*&& d[c.name]*/) {
      switch (c.type || 'text') {
        case 'date':
        case 'dateTime':

          if (c.dateRange) {
            // If a filter is set and the date is null, filter it out
            if (false == this.dateUtilsService.isValidDate(row.rowDatum[c.name])) {
              return false;
            }

            let start = this.dateUtilsService.getLocalISOTime(c.dateRange.start);
            let end = this.dateUtilsService.getLocalISOTime(this.dateUtilsService.addDays(c.dateRange.end, 1)); // increment day by 1
            if (!c.dateRange.end) {
              c.dateRange.end = this.dateUtilsService.getLocalISOTime(this.dateUtilsService.addDays(c.dateRange.start, 1));
            }

            if (!start) start = this.dateUtilsService.getLocalISOTime(this.dateUtilsService.MIN);
            if (!end) end = this.dateUtilsService.getLocalISOTime(this.dateUtilsService.MAX);

            // let value = typeof d[c.name] === 'Date' ? d[c.name] : new Date(d[c.name]);
            let value = this.dateUtilsService.getLocalISOTime(new Date(row.rowDatum[c.name]));
            // 

            // 
            return start <= value && value < end;
          } else {
            return true;
          }

        case 'dropdown':
        case 'text':
        case 'number':
        case 'currency':
          {
            let strval = ((c.valuePrepareFunction && c.filterPreparedValue) || c.type == 'currency') ? row.colHtmlTexts[c.name] : '' + row.rowDatum[c.name];
            strval = strval.toString();
            let value = c.filterIgnoreCase ? strval.toUpperCase() : strval;
            let filter = c.filter ? (c.filterIgnoreCase ? ('' + c.filter).toUpperCase() : '' + c.filter) : '';

            switch (c.filterMode) {
              case 'startsWith':
                return value.startsWith(filter);
              case 'allkeywords':
                return !filter || (filter && filter.split(' ').every(token => value.includes(token)));
              case 'contains':
              default:
                return value.includes(filter);
            }
          }

        case 'checkbox':
          {
            let value = row.rowDatum[c.name];
            let filter = c.filter;
            // 
            // 
            return value === filter;
          }
        case 'tag':
          {
            let filter = c.filter ? (c.filterIgnoreCase ? c.filter.toUpperCase() : c.filter) : '';
            if (filter) {
              if (c.filterPreparedValue) {
                let strval = row.rowDatum[c.name] || '';
                let value = c.filterIgnoreCase ? strval.toUpperCase() : strval;
                return value.includes(filter);
              } else {
                let tags: FancyTag[] = row.colHtmlTexts[c.name];
                return tags.some(tag => (tag.text ? (c.filterIgnoreCase ? tag.text.toUpperCase() : tag.text) : '').indexOf(filter) > -1);
              }
            }
          }
        // case 'text':
        //   if (false === StringUtils.isNullOrWhitespace(d[c.name])) {
        //     let value = d[c.name].toUpperCase();
        //     let filter = c.filter.toUpperCase();
        //     switch (c.filterMode) {
        //       case 'contains':
        //         return value.includes(filter);
        //       default:
        //         return value.startsWith(filter);
        //     }
        //   }
      }
    }

    // If no filters, return true
    return true;
  }

  public scrollTo(y: number) {
    if (document.querySelector('.tableContainer' + this.tableId)) {
      document.querySelector('.tableContainer' + this.tableId).scrollTop = y;
    }
  }

  public scrollToItem(itemIndex: number) {
    let currentVisibleRowsContainsItem = this.visibleData.some(d => d.index == itemIndex);
    if (currentVisibleRowsContainsItem) {
      let targetRow = this.allRows.find(r => r.index == itemIndex);
      //TODO:Optimize this, the problem here is the visibleData do contains the item but the uiSeq and itemIndex is not match.
      if (targetRow && !this.visibleData.some(d => d.uiSeq == targetRow.uiSeq)) {
        //check if the item already append to visible data
        while (this.visibleData.length < targetRow.uiSeq) {
          this.onInfinityScrollDown();
        }
      }
    }

    const el = document.getElementById(`${this.tableId}${itemIndex}`);
    if (el) {
      const tableContainer = document.querySelector('.tableContainer' + this.tableId);
      const finalScroll = el['offsetTop'] + SCROLLING_ROW_OFFSET;
      const initialScroll = tableContainer.scrollTop;
      const deltaScroll = finalScroll - initialScroll;

      if (this.settings.smoothScrollDurationMs > 0) {
        const duration = this.settings.smoothScrollDurationMs;
        const step = duration / 60; // 60 fps
        let current = 0;
        const scrollInterval = setInterval(() => {
          current += step;
          let scrollAmount = deltaScroll * this.smoothstep(0, duration, current);
          tableContainer.scrollTop = initialScroll + scrollAmount;
          if (current >= duration) {
            clearInterval(scrollInterval);
          }
        }, step);
      } else {
        tableContainer.scrollTop = finalScroll;
      }
      //el.scrollIntoView({ behavior: 'smooth' })
    }
  }

  private smoothstep(edge0: number, edge1: number, x: number) {
    // Scale, and clamp x to 0..1 range
    x = this.clamp((x - edge0) / (edge1 - edge0));

    return x * x * (3.0 - 2.0 * x);
  }

  private clamp(x: number, lowerlimit: number = 0.0, upperlimit: number = 1.0) {
    if (x < lowerlimit) return lowerlimit;
    if (x > upperlimit) return upperlimit;
    return x;
  }

  /**
   * Will return found rows count,
   * Example:`fancyTable.searchMultiRowsAndSelect(s => idArray.some(id=>id===s.id))`
   */
  public searchMultiRowsAndSelect(predicate: (value: T) => boolean): number {
    this.selectedRows = new Set();
    let count = 0;
    const searchResult = this.filteredRows.filter(d =>
      d.rowDatum && predicate(d.rowDatum));
    if (searchResult) {
      searchResult.forEach(row => {
        this.selectedRows.add(row);
      });
      count = searchResult.length;
      if (count > 0) {
        this.scrollToItem(this.allRows.indexOf(searchResult[0]));
      }
    }
    this.detectChanges();

    return count;
  }

  /**
   * Will return UI seq if found it other wise return `-1`, 
   * Example:`fancyTable.searchRowAndFocus(s => s.id == 'The id')`
   */
  public searchRowAndFocus(predicate: (value: T) => boolean, focusAfterDisplayDataRegen: boolean = false): number {
    if (focusAfterDisplayDataRegen) {
      this.afterDisplayDataGenerated.pipe(first()).subscribe(result => {
        this.searchRowAndFocus(predicate, false);
        this.detectChanges();
      });
      //this.reload();
    } else {
      let index = -1;
      const searchResult = this.filteredRows.find(d => d.rowDatum && predicate(d.rowDatum));
      if (searchResult) {
        index = searchResult.index;
      }
      this.setFocusedRowByIndex(index);
      this.detectChanges();
      return index;
    }
    return 0;
  }

  public setColEditableType(name: string, type: FtEditableType, fieldWidth = -1) {
    let col = this.settings.columns.find(c => c.name == name);
    if (col) {
      col.editableType = type;
      if (fieldWidth > -1) {
        col.editingFieldWidth = fieldWidth;
      }
    }
  }

  public setFocusedFilterInput(event: FocusEvent) {
    this.lastFocusedFilterInput = event ? <HTMLInputElement>event.target : null;
  }

  public setFocusedRowByIndex(index: number) {
    if (index === -1) {
      this.selectedRows = new Set();
      this.firstSelectedRow = undefined;
      this.rowSelected.emit(this.getSelected());
    }
    else if (this.filteredRows.length >= index + 1) {
      this.firstSelectedRow = this.filteredRows.find(r => r.index == index);
      this.selectedRows = new Set([this.firstSelectedRow]);
      this.rowSelected.emit(this.getSelected());

      //Need a UI change detection here but doesn't need to regenerate display data, doesn't need to call this.reload();
      this.detectChanges();

      this.scrollToItem(index);
    }
  }

  public setFocusedRowByUiSeq(uiSeq: number) {
    if (uiSeq === -1) {
      this.selectedRows = new Set();
      this.firstSelectedRow = undefined;
      this.rowSelected.emit(this.getSelected());
    }
    else {
      let focusRow = this.filteredRows.find(r => r.uiSeq == uiSeq);
      if (focusRow) {
        this.firstSelectedRow = focusRow;
        this.selectedRows = new Set([focusRow]);
        this.rowSelected.emit(this.getSelected());

        //Need a UI change detection here but doesn't need to regenerate display data, doesn't need to call this.reload();
        this.detectChanges();

        this.scrollToItem(focusRow.index);
      }
    }
  }

  public setSort(column: FancyColumn) {
    let sortDirection = 'asc';
    if (this.sortColumn &&
      this.sortColumn.name == column.name &&
      this.sortColumn.sortDirection === sortDirection) {
      sortDirection = 'desc';
    }

    this.sortColumn = { ...column } as FancyColumn;
    this.sortColumn.sortDirection = sortDirection;

    this.generateVisibleData();
    this.detectChanges();
    this.sortByColumn.next(this.filteredRows.filter(d => d.rowDatum).map(d => d.rowDatum));
  }

  public showHideGroup(groupKey) {
    let group = this.getGroupByKey(groupKey);

    group.expanded = !group.expanded;
    //this.anyExpanded = Object.keys(this.expandedGroups).some(k => this.expandedGroups[k]);
    this.generateVisibleData(false);
    if (group.expanded && group.rows.length > 1) {
      this.onInfinityScrollDown();
      this.detectChanges();
      setTimeout(() => {
        this.scrollToItem(group.rows[0].index);
      }, 200);
    }
  }

  // public selectAll() {
  //   if (0 < this.data.length) {
  //     delete this.selectedRows; // useful?
  //     this.firstSelectedRow =  this.data[0];
  //     this.selectedRows = new Set(this.data);
  //     this.reload();
  //   }
  // }
  public showProcessing() {
    this.processing = true;
    setTimeout(() => {
      this.processing = false;
    }, 300);
  }

  public slice(data: T[], start: number, length: number) {
    if (this.itemsPerPage > 0 && data) {
      return data.slice(start, start + length);
    }
    return data;
  }

  public sorted(data: FancyDisplayRow[]): FancyDisplayRow[] {
    if (data && data.length > 0) {
      if (this._sortBySeq) {
        return data.sort((a, b) => a.uiSeq - b.uiSeq);
      }
      else if (this.sortColumn) {
        // Todo: these values should be enum'd, or a function lookup table or something
        let descendingSort = this.sortColumn.sortDirection === 'desc';
        let doStringSort = (this.sortColumn.valuePrepareFunction && this.sortColumn.filterPreparedValue) || typeof (data[0].rowDatum[this.sortColumn.name]) === 'string';

        let doDateSort = this.sortColumn.type == 'date' || this.sortColumn.type == 'dateTime';
        let doTagSort = false;
        if (this.sortColumn.type == 'tag') {
          doTagSort = true;
          doStringSort = true;
        }

        // const sortFunctions = {typeof date.getMonth === 'function'
        //   'string': (a, b) => a.localeCompare(b),
        //   ... etc
        // }

        const sortedDataFunction = (a: FancyDisplayRow<T>, b: FancyDisplayRow<T>) => {
          if (this.sortColumn.sortFunction) {
            return this.sortColumn.sortFunction(a.rowDatum, b.rowDatum);
          }

          let aval = a.rowDatum[this.sortColumn.name];
          let bval = b.rowDatum[this.sortColumn.name];

          if (doTagSort) {
            aval = a.colTooltips[this.sortColumn.name];
            bval = b.colTooltips[this.sortColumn.name];

          } else if (this.sortColumn.valuePrepareFunction && this.sortColumn.filterPreparedValue) {
            aval = a.colHtmlTexts[this.sortColumn.name];
            bval = b.colHtmlTexts[this.sortColumn.name];
          }

          if (doDateSort) {
            if (doStringSort) {
              aval = this.dateUtilsService.parse(aval);
              bval = this.dateUtilsService.parse(bval);
            }
            return this.dateUtilsService.getTime(aval) - this.dateUtilsService.getTime(bval);
          } else if (doStringSort) {
            return StringUtils.compare(aval, bval);
          } else {
            return ((!aval && aval !== 0) || (!bval && bval !== 0)) ? 0 : aval - bval;
          }
        };

        if (descendingSort) {
          return data.sort((a, b) => sortedDataFunction(b, a)); // swapped a and b
        }
        else {
          return data.sort((a, b) => sortedDataFunction(a, b));
        }
      }
    }
    return data;
  }

  public sortedGroupKeys(groups: FancyGroup[]) {
    // 
    if (groups) {
      let descendingSort = this.settings.groupSortDirection === 'desc';// swapped a and b

      const sortFunction = (a: FancyGroup, b: FancyGroup) => {
        if (this.settings.groupSortFunction) {
          return descendingSort ? this.settings.groupSortFunction(b, a) : this.settings.groupSortFunction(a, b);
        }
        if (this.settings.groupSeqPrepareFunction) {
          return descendingSort ? (b.seq - a.seq) : (a.seq - b.seq);
        }
        switch (typeof (a.groupKey)) {
          case 'string':
            return descendingSort ? b.groupKey.localeCompare(a.groupKey) : a.groupKey.localeCompare(b.groupKey);
          default:
            return descendingSort ? (b.groupKey - a.groupKey) : (a.groupKey - b.groupKey);
        }
      };

      return groups.sort(sortFunction);
    }
    else {
      return [];
    }
  }

  public startDragging() {
    this.draggingRow = true;
  }

  public tagOnClick(row: FancyDisplayRow<T>, actionTag: string) {
    if (actionTag) {
      this.onTagClick.next({ row, actionTag });
    }
  }

  public toFancyColumn(v): FancyColumn {
    return v as FancyColumn;
  }

  public toggleCheckBox(taggingColumn: FancyColumn<T>, row: FancyDisplayRow<T>, $event: boolean, bulkUpdate = false) {
    let datum = row.rowDatum;
    datum[taggingColumn.name] = $event;
    if (taggingColumn.taggingTarget) {
      let rowId = datum[taggingColumn.taggingTarget];
      if ($event) {
        if (!this.settings.taggedValues.includes(rowId)) {
          this.settings.taggedValues.push(rowId);
          this.settings.taggedRowData.push(datum);
        }

        //TODO:The checkbox adding to selected rows still too buggy, circle back to this latter
        // if (!this.selectedRows.has(row)) {
        //   this.selectedRows.add(row);
        // }
        // this.firstSelectedRow = row;

      } else {
        let removeIndex = this.settings.taggedValues.indexOf(rowId);
        if (removeIndex > -1) {
          this.settings.taggedValues.splice(removeIndex, 1);
          this.settings.taggedRowData.splice(removeIndex, 1);
        }

        // if (this.selectedRows.has(row)) {
        //   this.selectedRows.delete(row);
        // }

        // if (this.selectedRows.size > 0) {
        //   this.firstSelectedRow = this.selectedRows[this.selectedRows.size - 1];
        // } else {
        //   this.firstSelectedRow = null;
        // }
      }

      if (!bulkUpdate) {
        this.rowSelected.emit(this.getSelected());
      }
    }
    if (!bulkUpdate) {
      let dataRows = this.filteredRows.filter(r => !r.isGroupHeader);
      let checkedRowCount = dataRows.filter(r => r.rowDatum[taggingColumn.name]).length;
      taggingColumn.uiCheckAll = checkedRowCount == dataRows.length ? true : checkedRowCount == 0 ? false : null;
      //this.checkAllBox.ngDoCheck();
      // if ($event) {
      //   this.setFocusedRowByIndex(row.index);
      // }
    }
    // if (!bulkUpdate && taggingColumn.taggingTarget && this.settings.taggedValues && this.settings.taggedValues.length > 0) {
    //   taggingColumn.uiCheckAll = null;
    //   this.checkAllBox.ngDoCheck();
    // }
    //taggingColumn.uiCheckAll=  taggingColumn.taggingTarget&& this.settings.taggedValues&& this.settings.taggedValues.length>0
  }

  public toggleExpandAll() {
    let expanded = !this.anyExpanded;
    this.groups.forEach(group => {
      group.expanded = expanded;
    });
    this.anyExpanded = expanded;
    this.generateVisibleData(false);
  }

  public trackByUiSeq(index: number, item: FancyDisplayRow) {
    return item.uiSeq;
  }

  public triggerDoubleClick(row: FancyDisplayRow<T>) {
    let selectedRows = this.getSelected();
    if (selectedRows.index === -1) {
      // if selection is disabled this should still return the row that was double clicked
      selectedRows = <FancySelection<T>>{
        index: row?.index ?? -1,
        focusedRow: row?.rowDatum,
        focusedCustomRowId: row?.customRowId,
        selectedRows: row ? [row.rowDatum] : [],
      };
    }
    this.doubleClick.emit(selectedRows);
    if (this.settings.editorTrigger == FtTrigger.DOUBLE_CLICK) {
      if (row && row.index > -1) {
        this.openEditor(row.index);
      }
    }
  }

  // #endregion Public Methods (68)

  // #region Private Methods (7)

  private calculateColWidth() {
    this.visibleColumns.forEach(col => {
      if (!col.group && col.visible) {
        col.uiWidth = this.getColWidthSetting(col);
      }
    });
  }

  private calculatePages(data: T[]) {
    if (this.itemsPerPage > 0 && data && data.length > 0) {
      this.maxPage = Math.ceil(data.length / this.itemsPerPage);
      this.pages = Array.from({ length: this.maxPage }, (x, i) => i + 1);
    }
    else {
      this.maxPage = 1;
      this.pages = [1,];
      this.page = 1;
    }
  }

  private getDatumFromTR(tr: Element): FancyDisplayRow {
    if (tr) {
      // 
      let rowId = tr.id.substr(this.tableId.length);
      // 

      let rowIndex = Number(rowId);
      // 

      return this.allRows[rowIndex];
    }
    else {
      return null;
    }
  }

  private getGroupKeyFromTR(tr: Element): FancyGroup<T> {
    if (tr && tr.attributes['groupkey']) {
      return this.groups.find(g => g.groupKey == tr.attributes['groupkey'].value);
    }
    else {
      return null;
    }
  }

  private getTrFromChild(element: Element): Element {
    if (element && element.id && element.id.startsWith(this.tableId)) {
      // console.log(element.id)
      return element;
    }
    else if (element && element.parentElement) {
      return this.getTrFromChild(element.parentElement);
    }
    else {
      return null;
    }
  }

  private initializeColumns() {
    if (this.settings && this.settings.columns) {
      this.sortColumn = this.settings.columns.find(c => !StringUtils.isNullOrWhitespace(c.sortDirection));
      this.visibleColumns = this.settings.columns.filter(c => c.visible && !c.group);
      this.groupColumn = this.settings.columns.find(c => c.group);

      this.calculateColWidth();
    }
  }

  private showDeletingConfirm(validationMsg: string, instance: FancyEditorInstance) {
    if (null === validationMsg) {
      this.msgBoxService.showConfirmDeleteBox().pipe(first()).subscribe(answer => {
        if (answer === true) {
          if (this.deleting.observed) {
            this.deleting.emit(instance);
          } else {
            // allow for default behavior if not subscribed to deleting event
            this.data.splice(instance.index, 1);
            this.reload();
          }
        }
      });
    }
    else {
      this.msgBoxService.show('Invalid Operation',
        { text: validationMsg, icon: ADIcon.WARNING, buttons: ADButtons.OK });
    }
  }

  // #endregion Private Methods (7)
}
