import { Component, Input, Output, EventEmitter, ChangeDetectionStrategy, ChangeDetectorRef, ViewChild, ElementRef, OnInit, HostListener } 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 { FancyTableEditorComponent } from './fancy-table-editor/fancy-table-editor.component';
import { first, take } from 'rxjs/operators';
import { ContextMenuItem } from '../../directives/right-click-menu/context-menu-item.model';
import { UuidUtils } from '../../utilities/uuid-utils';
import { MsgBoxService } from '../../services/msg-box.service';
import { DebounceTimer } from '../../utilities/timer-utils';
import { 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, Subscription } from 'rxjs';
import { NumberUtils } from '../../utilities/number-utils';
import { NbDialogService } from '@nebular/theme';
import { DateUtils } from '../../utilities/date-utils';

const DATE_FORMAT = 'MM/dd/yyyy'
const DATE_TIME_FORMAT = 'MM/dd/yyyy HH:mm'
const HTML_INNERTEXT_REGEX = />((.|\n*?))<\//g;
const SCROLLING_ROW_OFFSET = -150;
@Component({
  selector: 'fancy-table',
  templateUrl: './fancy-table.component.html',
  styleUrls: ['./fancy-table.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class FancyTableComponent<T = any> implements OnInit {
  @ViewChild('table', { static: true }) public tableToMeasureElement: ElementRef;

  private _data: T[];
  private _footerData: T[];
  private _footerSettings: FancySettings;
  private _itemsPerPage: number = 10;
  private _settings: FancySettings;
  private _sortBySeq: boolean = false;
  private _afterReloadSubscription: Subscription;
  lastFocusedFilterInput: HTMLInputElement;

  protected selectedRowIndex: number = undefined;

  public allRows: FancyDisplayRow[];
  public anyExpanded: boolean = true;

  /**
   * All generated displaying UI rows (Includes Group Header)
   */
  public displayData: FancyDisplayRow<T>[];
  public displayGroup: FancyGroup<T>[];
  public filteringDebounceTimer: DebounceTimer = new DebounceTimer(400, () => { this.generateVisibleData(); });
  public firstSelectedRow: FancyDisplayRow<T>;
  public footerDisplayData: FancyDisplayRow<T>[];
  public groupColumn: FancyColumn<T>;
  public groupValues = {};
  public groups: FancyGroup<T>[];
  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>[];


  /**
   * All visible displaying UI rows (Includes Group Header)
   */
  public visibleData: FancyDisplayRow[];
  @Input() public id!: string;
  @Output() public adding = new EventEmitter<FancyEditorInstance>();
  @Output() public deleting = new EventEmitter<FancyEditorInstance>();
  @Output() public doubleClick = new EventEmitter<FancySelection<T>>();
  @Output() public rowSelected = new EventEmitter<FancySelection<T>>();
  @Output() public saving = new EventEmitter<FancyEditorInstance>();
  @Output() public itemDrop = new EventEmitter<FancyDragDrop>();
  @Output() public pressEnter = new EventEmitter<FancySelection<T>>();
  @Output() public afterReload = new EventEmitter<T[]>();
  @Output() public afterDisplayDataGenerated = new EventEmitter<T[]>();
  @Output() public sortByColumn = new EventEmitter<T[]>();

  constructor(
    private el: ElementRef,
    private cdr: ChangeDetectorRef,
    private dialogService: NbDialogService,
    private msgBoxService: MsgBoxService
  ) { }

  public ngOnInit(): void {
  }

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

  public generateDisplayData() {
    this.displayData = [];
    this.allRows = [];
    if (this.settings && this.data) {
      this.visibleData = [];
      this.data.forEach((d, index) => {
        //d.index = index;
        this.allRows.push({
          seq: index,
          uiSeq: index,
          index: index,
          rowDatum: d,
          colHtmlTexts: {},//ObjectUtils.CloneValue(d),
          colTooltips: {},//ObjectUtils.CloneValue(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);
          });
        }
      });

      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.visibleColumns.forEach(col => {
        if (!col.group) {
          this.footerDisplayData.forEach(datum => {
            this.getFieldDisplayValue(datum, col);
            //datum[col.name] = col.valuePrepareFunction(datum);
          });
        }
      });
    }
  }

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

    this.filtered(this.allRows);

    //with grouping
    if (this.groupColumn /*&& !this.groups || Object.keys(this.groups).length === 0*/) {
      this.displayData = [];
      if (!this.groups || reGrouping) {
        this.groups = this.grouped(this.allRows.filter(d => d.visible));
        if (this.groupColumn.sortDirection) {
          this.groups = this.sortedGroupKeys(this.groups);
        }
      }

      //Adding Group header
      this.groups.forEach(group => {
        this.displayData.push({ isGroupHeader: true, groupKey: group.groupKey } as FancyDisplayRow);
        //Sort grouped data
        if (group.expanded) {
          this.displayData = this.displayData.concat(this.sorted(group.rows));
        }
      });
    }
    else {
      //Sorting
      this.displayData = this.sorted(this.allRows.filter(d => d.visible));
    }

    this.visibleData = this.displayData.slice(0, visibleRowCount);
    this.allRows.forEach(row => row.uiSeq = -1);
    this.displayData.forEach((row, i) => {
      row.uiSeq = i;
      if (row.isGroupHeader) this.getGroupByKey(row.groupKey).seq = i;
    });
    this.cdr.detectChanges();
  }

  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 getFieldDisplayValue(row: FancyDisplayRow, column: FancyColumn) {
    if (column.valuePrepareFunction) {
      let preparedValue = '' + column.valuePrepareFunction(row.rowDatum);
      row.colHtmlTexts[column.name] = preparedValue;
    } else {

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

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

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

        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;
        default:
          row.colHtmlTexts[column.name] = row.rowDatum[column.name];
          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 expandedGroups(key: string): boolean {
    let group = this.getGroupByKey(key);
    return group && group.expanded;
  }

  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.groupValuePrepareFunction ? this.groupColumn.groupValuePrepareFunction(row.rowDatum) : row.rowDatum[this.groupColumn.name];
        if (!row.groupKey) {
          //TODO:add configuration
          row.groupKey = 'N/A';
        }
      });

      let groups = LinqUtils.GroupBy(data, 'groupKey');

      let tempList: FancyGroup[] = [];
      var groupCount = 0;
      Object.entries(groups).forEach(([key, value]) => {
        tempList.push({
          groupKey: key,
          seq: value[0].seq + groupCount,
          groupValue: value[0].rowDatum[this.groupColumn.name],
          rows: value, expanded: true
        } as FancyGroup);
        groupCount++;
      });
      this.groups = tempList;
    }

    return this.groups;
  }

  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) {
      this.scrollToItem(group.rows[0].index);
    }
  }
  public sortedGroupKeys(groups: FancyGroup[]) {
    // 
    if (groups) {
      const sortFunction = (a: FancyGroup, b: FancyGroup) => {
        if (this.settings.groupSortFunction) {
          return this.settings.groupSortFunction(a, b);
        }

        switch (typeof (a.groupValue)) {
          case 'string':
            return a.groupValue.localeCompare(b.groupValue);
          default:
            return a.groupValue - b.groupValue;
        }
      };

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

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

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

  public getColWidthSetting(col: FancyColumn) {
    if (col.type == 'checkall') {
      return '36px';
    } 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 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.cdr.markForCheck();
      return true;
    }
    return false;
  }

  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 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.cdr.markForCheck();
    this.sortByColumn.next(this.displayData.filter(d => d.rowDatum).map(d => d.rowDatum));
  }
  public resetSort() {
    this.sortColumn = null;
    if (this.settings && this.settings.columns) {
      this.sortColumn = this.settings.columns.find(c => !StringUtils.isNullOrWhitespace(c.sortDirection));
    }
  }
  private _draggingLeftColIndex: number;
  private _draggingLeftColClientWidth: number;
  private _draggingRightColClientWidth: number;
  colDragResizing: boolean = false;

  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;
  }

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

  colResizeDragResize(col: FancyColumn, $event: CdkDragMove<T>) {
    let leftColumn = this.visibleColumns[this._draggingLeftColIndex];
    let rightColumn = this.visibleColumns[this._draggingLeftColIndex + 1];

    let offsetX = $event.distance.x;
    leftColumn.widthPct = 0;
    leftColumn.widthRem = 0;
    leftColumn.widthPx = this._draggingLeftColClientWidth + offsetX;

    rightColumn.widthPct = 0;
    rightColumn.widthRem = 0;
    rightColumn.widthPx = this._draggingRightColClientWidth - offsetX;
  }

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

  public drop(event: CdkDragDrop<T[]>) {
    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;


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

    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.displayData.indexOf(selectedRows[0]) + i - 1);

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

      moveItemInArray(this.displayData, this.displayData.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;
        row.rowDatum[this.groupColumn.name] = currentGroup.groupValue;

        transferArrayItem(previousGroup.rows, currentGroup.rows, previousGroup.rows.indexOf(row), 0)
      }
    }
    this.displayData.forEach((row, i) => {
      row.uiSeq = i;
      row.seq = i;
    });
    //Enable sort by Seq after drag
    this._sortBySeq = true;
    this.generateVisibleData(false);
    this.itemDrop.emit({ previousIndex: event.previousIndex, currentIndex: event.currentIndex, previousGroup, currentGroup });
  }

  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';

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

        const sortedDataFunction = (a: any, b: any) => {
          let aval = a.rowDatum[this.sortColumn.name];
          let bval = b.rowDatum[this.sortColumn.name];

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

          if (this.sortColumn.sortFunction) {
            return this.sortColumn.sortFunction(aval, bval);
          }

          if (doStringSort) {
            return aval ? aval.localeCompare(bval) : false;
          } 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 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':
          // If a filter is set and the date is null, filter it out
          if (StringUtils.isNullOrWhitespace(row.rowDatum[c.name])) {
            return false;
          }

          if (c.dateRange !== undefined) {
            let start = c.dateRange.start;
            let end = DateUtils.addDays(c.dateRange.end, 1); // increment day by 1

            // if (!start) start = DateUtils.getLocalISOTime(DateUtils.MIN);
            // if (!end) end = DateUtils.getLocalISOTime(DateUtils.MAX);

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

            // 
            return start <= value && value < end;
          }

        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];

            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 (c.filterPreparedValue) {
              let tags: FancyTag[] = row.colHtmlTexts[c.name];
              return tags.some(tag => (tag.text ? (c.filterIgnoreCase ? tag.text.toUpperCase() : tag.text) : '').indexOf(filter) > -1);
            } else {

              let strval = row.rowDatum[c.name] || '';
              let value = c.filterIgnoreCase ? strval.toUpperCase() : strval;
              return value.includes(filter);
            }
          }
        // 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 setFocusedRowByIndex(index: number) {
    if (index === -1) {
      this.selectedRows = new Set();
      this.firstSelectedRow = undefined;
      this.rowSelected.emit(this.getSelected());
    }
    else if (this.data.length >= index + 1) {
      this.firstSelectedRow = this.allRows[index];
      this.selectedRows = new Set([this.allRows[index]]);
      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.cdr.detectChanges();

      this.scrollToItem(index);
    }
  }

  public setFocusedRowByUiSeq(uiSeq: number) {
    if (uiSeq === -1) {
      this.selectedRows = new Set();
      this.firstSelectedRow = undefined;
      this.rowSelected.emit(this.getSelected());
    }
    else if (this.displayData.length >= uiSeq + 1) {
      this.firstSelectedRow = this.displayData[uiSeq];
      this.selectedRows = new Set([this.displayData[uiSeq]]);
      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.cdr.detectChanges();

      this.scrollToItem(this.displayData[uiSeq].index);
    }
  }

  /**
   * 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.cdr.detectChanges();
      });
      //this.reload();
    } else {
      let index = -1;
      const searchResult = this.displayData.find(d => d.rowDatum && predicate(d.rowDatum));
      if (searchResult) {
        index = searchResult.index;
      }
      this.setFocusedRowByIndex(index);
      this.cdr.detectChanges();
      return index;
    }
    return 0;
  }


  /**
   * 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.displayData.filter(d =>
      d.rowDatum && predicate(d.rowDatum));
    if (searchResult) {
      searchResult.forEach(row => {
        this.selectedRows.add(row);
      });
      count = searchResult.length;
    }
    this.cdr.detectChanges();
    return count;
  }

  public checkboxCheckAll(colName: string, checked: boolean) {
    this.data.forEach(datum => {
      datum[colName] = checked;
    });
    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 get anyFiltered(): boolean {
    //null check for checkbox equals false
    return this.settings.columns.some(c => c.filter != null && c.filter != '');
  }

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

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

  @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);
  }

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

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

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

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

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

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

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

  @Input() public set settings(value: FancySettings) {
    this._settings = new FancySettings({ ...value });
    this.initializeColumns();
    if (this.data && !this.visibleData) {
      this.generateDisplayData();
    }
    // if (this.anyGroups) {
    //   
    //   this.grouped(this.filtered(this.data));
    // }
  }
  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;
  }

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

  public addRow(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 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);
    }
  }

  private showDeletingConfirm(validationMsg: string, instance: FancyEditorInstance) {
    if (null === validationMsg) {
      this.msgBoxService.showConfirmDeleteBox().pipe(first()).subscribe(answer => {
        if (answer === true) {
          this.deleting.emit(instance);
        }
      });
    }
    else {
      this.msgBoxService.show('Invalid Operation', { text: validationMsg });
    }
  }

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

  public getSelected() {
    return <FancySelection<T>>{
      index: this.firstSelectedRow.index,
      focusedRow: this.firstSelectedRow.rowDatum,
      selectedRows: Array.from(this.selectedRows.keys()).map(r => r.rowDatum) as T[],
    }
  }

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

  // <tbody> has this [RightClickValue]="this", so "self" is "this" FancyTableComponent
  // "this" is not in the click MouseEvent scope, afaik
  public onContextMenuOpening(self: any, 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);


    let targetDatum = self.getDatumFromTR(targetRow);


    let rowDatum = targetDatum ? targetDatum.rowDatum : null;
    self.onRowSelect(targetDatum ? targetDatum.index : -1, event);
    if (self.settings.onContextMenuOpening) {
      self.settings.onContextMenuOpening(rowDatum, rightClickMenuItems, event);
    }
    return rowDatum;
  }

  public onInfinityScrollDown() {
    this.visibleData = this.visibleData.concat(this.displayData.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 onRowSelect(index: number, event: MouseEvent) {
    //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)) {
      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());
  }

  public openEditor(editingIndex: number = undefined) {

    if (undefined === editingIndex) {
      // selectedRowIndex = this.data.findIndex(d => this.selectedRows.has(d));
      editingIndex = this.firstSelectedRow.index;
    }

    if (this.settings.columns.some(c => c.editableType == FtEditableType.EDITABLE)) {

      let onEditorClosed = this.dialogService.open(FancyTableEditorComponent, {
        context: {
          columns: this.settings.columns,
          datum: this.data[editingIndex],
          index: editingIndex,
          validation: this.settings.editingValidation
        },
      }).onClose;
      onEditorClosed.pipe(take(1)).subscribe(instance => {
        if (instance) {
          this.saving.emit({ index: editingIndex, object: instance });
        }
      });
      return onEditorClosed;
    }
    return of(false);
  }

  public reload() {

    this.initializeColumns();
    this.generateDisplayData();
    // this.groups = undefined;
    this.cdr.markForCheck();
    this.afterReload.emit();
  }

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

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

  public scrollToItem(itemIndex: number) {
    //check if the item already append to visible data
    while (this.visibleData.length < this.allRows[itemIndex].uiSeq) {
      this.onInfinityScrollDown();
    }

    const el = document.getElementById(`${this.tableId}${itemIndex}`);
    if (el) {
      document.querySelector('.tableContainer' + this.tableId).scrollTop = el['offsetTop'] + SCROLLING_ROW_OFFSET;
      //el.scrollIntoView({ behavior: 'smooth' })
    }
  }

  // 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 toFancyColumn(v): FancyColumn {
    return v as FancyColumn;
  }

  public triggerDoubleClick() {
    let selectedRows = this.getSelected();
    this.doubleClick.emit(selectedRows);
    if (this.settings.editorTrigger == FtTrigger.DOUBLE_CLICK) {
      this.openEditor(this.firstSelectedRow.index);
    }
  }

  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 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);
      this.groupColumn = this.settings.columns.find(c => c.group);
    }

  }
  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 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;
    }
  }

  //@HostListener('keydown', ['$event'])
  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;
    }
  }

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

}
