import { TranslatedPropertyHelper } from './../utils/translated-property-helper';
import { CustomAgGridOptions } from '../resources/elements/ag-grid/custom-ag-grid-options';
import { EntityManager } from './../data/entity-manager';
import { ViewModelBase } from './view-model-base'
import { ServiceBase } from '../data/service-base';
import * as breeze from 'breeze-client';
import { Router } from 'aurelia-router';
import { CustomLogger } from '../modules/custom-logger';
import { ColDef, ColumnState, ModelUpdatedEvent } from "ag-grid-community";
import { observable, Container, ViewCompiler, computedFrom, ViewSlot, ViewResources, createOverrideContext } from 'aurelia-framework';
import { Box } from '../modules/box';
import { Subscription } from 'aurelia-event-aggregator';
import { AgGridView, DataGridSetting, FieldType } from '../model/common-model';
import { AgGridViewService } from '../services/ag-grid-view-service';
import { AgGridSortingHelper, GridOptions } from '../resources/elements/utilities/ag-grid-helpers';
import { Config } from '../config';
import { autoAdjustColumnsSize, getNewColumnStates } from './../ag-grid/agUtils';
import moment from 'moment';
import { CamelCase } from '../utils/came-case';

export class SelectAllHeaderComponent {
  params: ListViewModelBase<breeze.Entity>;
  eGui: any;
  // optional method, gets called once with params
  init?(params: any): void {
    this.params = params.VM;
    this.eGui = this.params.headerCellRendererSelectAll();
  }
  // gets called once, you should return the HTML element
  getGui(): HTMLElement {
    return this.eGui;
  }
  // optional method, gets called once, when component is destroyed
  destroy?(): void {
  }
}

export abstract class ListViewModelBase<T extends breeze.Entity> extends ViewModelBase { // <<<--- Attention: remplacé 'Model.BaseEntityWithId' par 'breeze.Entity'
  public abstract get ressourceName(): string;

  public gridViewService: AgGridViewService;
  public agGridViewList: Array<AgGridView>;
  public viewCompiler: ViewCompiler;
  public box: Box;
  public localeChangedSubscription: Subscription;
  protected gridOptions: GridOptions;
  public service: ServiceBase<T>;
  public manager: breeze.EntityManager;
  protected routeName: string;
  private isViewsEnabled: boolean;
  private idSuffix = `${Date.now()}`;
  public useUserSettingFilter: boolean = true;
  public useUserSettingSorter: boolean = true;
  private firstIndex: number;
  private isFirstTimeSelection: boolean;
  protected usedViewId: number;
  protected agGridSortingHelper: AgGridSortingHelper;
  public usedView: AgGridView;
  private showUsedViewInList: boolean;
  public selectedEntitiesCount: number;
  public canSelectAll: boolean = true
  @observable
  protected selectAll: boolean;
  private translatedPropertyHelper: TranslatedPropertyHelper;
  /**
  * @experimental The variable should only be used for experimental purposes
  */
  public useExperimentalProjection: boolean = false;
  public additionalSelectPropertyPaths: Array<string> = new Array<string>();

  constructor(router: Router, logger: CustomLogger, serviceInstance: ServiceBase<T>) {
    super(router, logger);
    this.service = serviceInstance;
    this.manager = Container.instance.get(EntityManager).manager;
    this.box = Container.instance.get(Box);
    this.viewCompiler = Container.instance.get(ViewCompiler);
    this.gridViewService = new AgGridViewService();
    this.showUsedViewInList = Container.instance.get(Config).agGridConfig.showUsedViewInList;
    this.translatedPropertyHelper = Container.instance.get(TranslatedPropertyHelper);

    this.localeChangedSubscription = this.eventAggregator.subscribe('i18n:locale:changed', payload => {
      let fragment = this.router.currentInstruction.fragment;
      if (this.router.currentInstruction.queryString != null) {
        fragment += '?' + this.router.currentInstruction.queryString;
      }
      this.router.navigate(fragment, { replace: true, trigger: true });
    });

    this.initializeGridOptions(this.config.agGridConfig.showFixedFilter);
    this.agGridSortingHelper = new AgGridSortingHelper(this.gridOptions);
  }

  public unselectAll() {
    this.gridOptions?.api?.forEachNode((rowNode) => {
      if (rowNode.data != null && false != rowNode.data.isSelected) {
        rowNode.data.isSelected = false;
      }
    });
  }

  public async attached() {
    this.routeName = this.router.currentInstruction.config.route as string;
    this.isViewsEnabled = this.router.currentInstruction.config.settings.isViewsEnabled;
    if (this.isViewsEnabled == null) this.isViewsEnabled = true;
    window.addEventListener('beforeunload', () => this.saveGridColumnsState());
  }

  public async detached() {
    super.detached();
    await this.saveGridColumnsState();
    window.removeEventListener('beforeunload', () => this.saveGridColumnsState());
  }

  public get ribbonHeaderText() {
    this.usedView = this.agGridViewList?.find(x => x.used);
    let title = this.i18n.tr(this.router.currentInstruction.config.navModel.title);
    if (this.showUsedViewInList) {
      let viewName = (this.usedView != null && !this.usedView.isDefaultView) ? ` - ${this.i18n.tr("view.view", { ns: "common" })} : ${this.usedView.name}` : '';
      return title + viewName;
    }
    return title;
  }

  public get selectedEntities(): T[] {
    if (this.gridOptions == null || this.gridOptions.api == null) return [];
    let selectedEntities = [];
    this.gridOptions.api.forEachNode((rowNode) => {
      if (rowNode.data != null)
        if (rowNode.data.isSelected == true)
          selectedEntities.push(rowNode.data);
    });
    this.selectedEntitiesCount = selectedEntities.length;
    return selectedEntities;
  }

  public get entitiesCount() {
    if (this.gridOptions == null || this.gridOptions.api == null) return 0;
    return this.gridOptions.api.getInfiniteRowCount();
  }

  @computedFrom("authService", "selectedEntities")
  public get canDelete() {
    return this.selectedEntities.length > 0 && this.authService.checkAccess(this.ressourceName, 'delete');
  }

  public async getSelectedEntitiesFromCache(): Promise<T[]> {
    let entitiesFromCache = await this.service.getEntitiesFromLocalCache();
    return entitiesFromCache.filter(x => (x as any).isSelected);
  }

  public async delete(detachInverseFKEntities: boolean = true): Promise<boolean> {
    let isDeleted = false;
    if (this.selectedEntities.length > 0) {
      let entitiesToDelete = this.selectedEntities;
      //Fetch entities when using 'useExperimentalProjection' to use breeze deleteEntities function
      if (this.useExperimentalProjection && entitiesToDelete[0].hasOwnProperty("id")) {
        entitiesToDelete = await this.service.getEntities(breeze.Predicate.or(this.selectedEntities.map((selectedEntity: T & { id: number }) => new breeze.Predicate("id", breeze.FilterQueryOp.Equals, selectedEntity.id))));
      }

      isDeleted = await this.service.deleteEntities(entitiesToDelete, detachInverseFKEntities);
      if (isDeleted) {
        this.refreshAfterDelete();
      }
    }
    return isDeleted;
  }

  protected refreshAfterDelete() {
    this.gridOptions.api.forEachNode((rowNode) => {
      if (rowNode.data != null)
        if (rowNode.data.isSelected == true)
          rowNode.data.isSelected = false;
    });
    this.gridOptions.api.refreshInfiniteCache();
  }

  public deactivate() {
    this.unselectAll();
    this.localeChangedSubscription.dispose();
  }

  @observable
  public filter: string;
  public filterChanged(newValue, oldValue) {
    this.service.gridDataSource.quickFilter = newValue;
    this.gridOptions.api.onFilterChanged();
  }

  selectAllChanged(newValue, oldValue) {
    if (this.gridOptions.api && newValue != oldValue) {
      let MAX_SELECTION = 1000;
      let infiniteRowCount = this.gridOptions.api.getInfiniteRowCount();


      if (newValue) {
        if (infiniteRowCount > MAX_SELECTION) {
          let titleWarning = this.i18n.tr('grid.warning', { ns: "common" });
          let str = this.i18n.tr('grid.selectionRejected', { ns: "common", amount: infiniteRowCount, max: MAX_SELECTION });
          this.box.showWarning(str, titleWarning);
          this.selectAll = false;
          return;
        }


        if (!this.allDataLoaded()) {
          // Forcer le chargement de toutes les données côté client
          this.gridOptions.cacheBlockSize = infiniteRowCount;

          let infiniteModel = (this.gridOptions.api.getModel() as any);
          let initialFnUpdate = this.gridOptions.onModelUpdated;
          this.gridOptions.onModelUpdated = (event: ModelUpdatedEvent) => {
            console.log(event); // => Es que ce console.log est utile ?
            let currentRowCount = this.gridOptions.api.getModel().getRowCount();
            if (infiniteRowCount <= currentRowCount) {
              this.gridOptions.api.hideOverlay();
              this.actualizeCheckboxes();
              let str = this.i18n.tr('grid.elementsSelected', { ns: "common", amount: this.getSelectableRows().length });
              this.log(str, null, true);
              infiniteModel.onCacheUpdated = initialFnUpdate;
            }
          }
          (this.gridOptions.api.getModel() as any).reset();
        }
      }
      this.actualizeCheckboxes();
    }
  }

  private gotoDetails(self, entity: T) {
    var url = self.getDetailsUrl(self, entity);
    if (url) {
      self.router.navigate(url);
    }
  }

  private gotoDetailInNewWindow(self, entity: T) {
    var url = self.getDetailsUrl(self, entity);
    if (url) {
      window.open(url, "blank");
    }

  }

  private openInNewTab(self, entity: T) {
    var url = this.getDetailsUrl(self, entity);
    if (url) {
      window.open(url, "_blank");
    }
  }

  public getDetailsUrl(self, entity: T): string {
    return null;
  }

  public onCellClicked(entity: T): boolean | null {
    return true;
  }

  public initializeGridOptions(fixedFilters: boolean = false, hasHyperlinkColumn: boolean | null = null) {
    if (!this.service) return;
    this.gridOptions = <GridOptions>{};

    this.gridOptions.rowModelType = 'infinite';
    this.gridOptions.pagination = true;
    this.gridOptions.paginationAutoPageSize = true;
    this.gridOptions.fixedFilters = fixedFilters;
    this.gridOptions.suppressDragLeaveHidesColumns = true;

    this.gridOptions.maxConcurrentDatasourceRequests = 3;
    this.gridOptions.rowHeight = 40;
    this.gridOptions.headerHeight = 45;
    this.gridOptions.floatingFiltersHeight = 45;
    this.gridOptions.animateRows = true;

    this.gridOptions.rowSelection = 'multiple';
    this.gridOptions.suppressRowClickSelection = true;
    this.gridOptions.enableBrowserTooltips = true;
    this.gridOptions.multiSortKey = 'ctrl';
    this.gridOptions.components = CustomAgGridOptions.Components;
    this.gridOptions.columnTypes = CustomAgGridOptions.ColumnTypeDefinitions(this.i18n, this.config.globalConfig.defaultLocale);
    let colDefs = this.getDataGridColumns();
    if (this.useExperimentalProjection) {
      this.applySelect(colDefs);
    }
    this.gridOptions.columnDefs = colDefs;
    hasHyperlinkColumn = hasHyperlinkColumn === null ? this.config.agGridConfig.hasHyperlinkColumn : hasHyperlinkColumn;
    if (this.gridOptions.columnDefs != null && hasHyperlinkColumn) {
      this.gridOptions.columnDefs.push({
        headerName: "",
        field: "hyperlink",
        maxWidth: 40,
        minWidth: 40,
        cellRendererSelector: (params) => {
          return {
            component: "hyperlinkRenderer",
            params: { value: this.getDetailsUrl(this, <T>params.data), linkInNewTab: this.config.agGridConfig.hyperlinkInNewTab }
          }
        },
        suppressColumnsToolPanel: true,
        suppressMenu: true,
        sortable: false,
        resizable: false,
        filter: false,
        lockPosition: "right",
        pinned: "right"
      });
    }
    this.gridOptions.columnDefs.forEach(columnDef => {
      columnDef.headerTooltip = columnDef.headerName;
    });
    this.gridOptions.onCellClicked = (event?) => { this.onCellClickedInternal(event); };
    this.gridOptions.onModelUpdated = (event?) => { this.onModelUpdated(event); };
    this.gridOptions.onSelectionChanged = (event?) => { this.onSelectionChanged(event); }
    this.gridOptions.onGridReady = (event?) => { this.onGridReady(event); };
    this.gridOptions.getLocaleText = (input) => this.i18n.tr("grid." + input.key, { ns: "common" });
    if (this.config.agGridConfig.suppressMenu) {
      this.gridOptions.defaultColDef = {
        suppressMenu: true
      };
    }
    else {
      this.gridOptions.defaultColDef = {
        suppressMenu: false,
        menuTabs: ['filterMenuTab']
      };
    }

    this.gridOptions.enableCellTextSelection = true;
  }

  private onCellClickedInternal(event?) {
    let mouseEvent: PointerEvent = event.event;
    if (event.colDef.onCellClicked == null) {
      if ((event.colDef as ColDef).pinned == null || (event.colDef as ColDef).pinned === false) {
        if (this.onCellClicked(<T>event.data) !== false) {
          this.gotoDetails(this, <T>event.data);
        }
      }
      else if (event.colDef.field == 'isSelected') {
        let inputs = mouseEvent.target.getElementsByTagName("input");
        if (inputs.length > 0 && inputs.item(0).getAttribute("type") == "checkbox") {
          if (!mouseEvent.shiftKey || this.firstIndex == null) {
            this.firstIndex = event.rowIndex;
            this.isFirstTimeSelection = true;
          }
          else {
            let secondIndex: number = event.rowIndex;
            this.gridOptions.api.forEachNode((rowNode, index) => {
              if (rowNode.data != null) {
                if (this.firstIndex >= secondIndex) {
                  if (rowNode.rowIndex > secondIndex && ((this.isFirstTimeSelection && rowNode.rowIndex < this.firstIndex) || (!this.isFirstTimeSelection && rowNode.rowIndex <= this.firstIndex))) {
                    rowNode.data.isSelected = !rowNode.data.isSelected;
                  }
                }
                else {
                  if (((this.isFirstTimeSelection && rowNode.rowIndex > this.firstIndex) || (!this.isFirstTimeSelection && rowNode.rowIndex >= this.firstIndex)) && rowNode.rowIndex < secondIndex) {
                    rowNode.data.isSelected = !rowNode.data.isSelected;
                  }
                }
                if (rowNode.rowIndex - 1 == this.firstIndex) this.isFirstTimeSelection = false;
              }
            });
          }
        }
      }
    }
    if (event.colDef.field == 'isSelected') this.onSelectionChanged(event);
  }

  private onModelUpdated(event?) {
    if (this.gridOptions?.adjustColumns === true && this.gridOptions.api) {
      this.gridOptions.api.sizeColumnsToFit();
    }

    this.actualizeCheckboxes();
  }

  public onSelectionChanged(event?) {
    this.checkHeaderCheckBoxIfNeeded();
    this.handleSingleRowSelectionMode(event);
  }

  private handleSingleRowSelectionMode(event: any) {
    if (this.gridOptions.rowSelection != 'multiple' && event.data.isSelected === true) {
      this.gridOptions.api.forEachNode((rowNode) => {
        if (rowNode.data != null && rowNode.data != event.data)
          if (rowNode.data.isSelected == true)
            rowNode.data.isSelected = false;
      });
    }
  }

  private checkHeaderCheckBoxIfNeeded() {
    var selCount = this.selectedEntities.length;
    // Only take selectable rows
    var rowCount = this.getSelectableRows().length;
    if (selCount == rowCount) {
      // Header checkbox --> checked
      this.selectAll = true;
    }
    else {
      // Header checkbox --> unchecked
      this.selectAll = false;
    }
  }

  private async applyViewById(viewId: number) {
    let usedView = await this.gridViewService.getEntityById(viewId);
    if (usedView != null && this.agGridViewList != null) {
      usedView.used = true;
      this.agGridViewList.push(usedView);
      this.applyView(usedView);
    } else {
      this.applyDefaultView();
    }
  }

  private applyDefaultView() {
    if (this.agGridViewList?.length > 0) {
      this.applyView(this.agGridViewList.find(x => x.isDefaultView) ?? this.agGridViewList[0]);
      autoAdjustColumnsSize(this.gridOptions);
    }
  }

  private applyView(view: AgGridView) {
    if (view != null) {
      this.gridOptions.columnApi.applyColumnState({
        state: getNewColumnStates(this.gridOptions, JSON.parse(view.columnState) as ColumnState[]),
        defaultState: { sort: null },
        applyOrder: true
      });
      this.gridOptions.api.setFilterModel(JSON.parse(view.filterModel));
      this.gridOptions.fixedFilters = view.fixedFilters && !this.config.agGridConfig.showFilterBox;
      this.gridOptions.adjustColumns = view.adjustColumns;
    }
  }

  private applyCustomSettings(settings: DataGridSetting): boolean {
    if (settings.colsState?.length > 0) {
      this.gridOptions.columnApi.applyColumnState({
        state: getNewColumnStates(this.gridOptions, settings.colsState),
        defaultState: { sort: null },
        applyOrder: true
      });
    }
    let forceOpenFilterRow = false;
    if (this.useUserSettingFilter && settings.filterModel) {
      this.gridOptions.api.setFilterModel(settings.filterModel);
      // In case of custom filters, show them by default
      if (Object.keys(settings.filterModel || {}).length > 0) {
        forceOpenFilterRow = !this.config.agGridConfig.showFilterBox;
      }
    }
    if (settings.fixedFilters != null) {
      this.gridOptions.fixedFilters = settings.fixedFilters && !this.config.agGridConfig.showFilterBox;
    }
    if (settings.adjustColumns != null) {
      this.gridOptions.adjustColumns = settings.adjustColumns;
    }
    return forceOpenFilterRow;
  }

  public async onGridReady(event?) {
    if (this.router.currentInstruction != null) {
      await this.initAgGridView();

      let forcedViewId = this.usedViewId ?? this.router.currentInstruction.queryParams["usedViewId"];
      if (forcedViewId) {
        await this.applyViewById(forcedViewId);
      }
      let savedSettings: DataGridSetting = this.authService.currentUser ? this.authService.currentUser.sanitizedSettings.dataGridSettings[this.routeName] : null;
      let forceOpenFilterRow = false;
      if (savedSettings != null) {
        if (savedSettings.usedView != null) {
          await this.applyViewById(savedSettings.usedView.id);
        }
        forceOpenFilterRow = this.applyCustomSettings(savedSettings);
      }

      if (forcedViewId || savedSettings) {
        if (false == this.agGridSortingHelper.hasAnySorting()) {
          this.agGridSortingHelper.applyDefaultSortModel();
        }
      } else {
        this.applyDefaultView();
      }
      this.gridOptions.fixedFilters = this.gridOptions.fixedFilters ?? false;
      this.toggleFloatingRow(forceOpenFilterRow || this.gridOptions.fixedFilters);
      this.gridOptions.adjustColumns = this.gridOptions.adjustColumns ?? this.config.agGridConfig.defaultAdjustColumns;
    }

    var onDisplayedColumnsChangedHandler = this.gridOptions.onDisplayedColumnsChanged;
    this.gridOptions.onDisplayedColumnsChanged = (ev) => {
      if (onDisplayedColumnsChangedHandler != null) onDisplayedColumnsChangedHandler(ev);
    };

    if (this.gridOptions.api) {
      this.gridOptions.api.refreshHeader();
      this.gridOptions.api.setDatasource(this.service.gridDataSource);
    }
  }

  public toggleFloatingRow(floatingFilter: boolean = null) {
    if (this.gridOptions) {
      let columnDefs = this.gridOptions.api.getColumnDefs();
      columnDefs.forEach((columnDef: ColDef) => {
        columnDef.floatingFilter = floatingFilter ?? !columnDef.floatingFilter;
      });
      this.gridOptions.api.setColumnDefs(columnDefs)
      this.gridOptions.api.refreshHeader();
    }
  }

  protected async saveUserSettings(settings: DataGridSetting) {
    if (this.authService.currentUser) {
      this.authService.currentUser.settings.dataGridSettings[this.routeName] = settings;
      await this.datacontext.saveUserSettings(this.authService.currentUser);
    }
  }

  protected async saveGridColumnsState() {
    if (this.authService.currentUser) {
      // Selected view is the currently selected view. If no select, it is the default. If no views used, it is null.
      let selectedView = this.isViewsEnabled ? this.agGridViewList?.find(x => x.used) ?? this.agGridViewList.find(x => x.isDefaultView) : null;

      let savedSettings = this.authService.currentUser.settings.dataGridSettings[this.routeName];
      let currentSettings: DataGridSetting = {
        usedView: selectedView && selectedView.id > 0 ? { id: selectedView.id, name: selectedView.name } : null,
        fixedFilters: this.gridOptions.fixedFilters,
        adjustColumns: this.gridOptions.adjustColumns
      };
      if (this.gridOptions.columnApi != null) {
        currentSettings.colsState = this.filterColStateForSaving(this.gridOptions.columnApi.getColumnState());
      }
      if (this.gridOptions.api != null) {
        currentSettings.filterModel = JSON.parse(JSON.stringify(this.gridOptions.api.getFilterModel()));
      }

      // If there is a view selected, only save cols, fixed, adjust and filter if different from view
      if (selectedView != null) {
        if (this.isFixedFiltersEqual(currentSettings, selectedView)) {
          currentSettings.fixedFilters = null;
        }
        if (this.isAdjustColumnsEqual(currentSettings, selectedView)) {
          currentSettings.adjustColumns = null;
        }
        if (this.isColStateEqual(currentSettings, { colsState: this.filterColStateForSaving(JSON.parse(selectedView.columnState)) })) {
          currentSettings.colsState = null;
        }
        if (this.isFilterModelEqual(currentSettings, { filterModel: JSON.parse(selectedView.filterModel) })) {
          currentSettings.filterModel = null;
        }
      }

      if (Object.keys(currentSettings.filterModel || {}).length == 0) {
        currentSettings.filterModel = null;
      }

      if (savedSettings == null || this.isSavedDifferentFromCurrent(savedSettings, currentSettings)) {
        await this.saveUserSettings(currentSettings)
      }
    }
  }

  protected filterColStateForSaving(colState: any[] | null) {
    return (colState || []).filter((x: ColumnState & { pinned?: boolean }) =>
      (x.pinned == null || x.pinned === false) && x.colId != 'isSelected' && x.colId != 'hyperlink'
    );
  }

  protected isSavedDifferentFromCurrent(settingsA: DataGridSetting, settingsB: DataGridSetting) {
    return settingsA.usedView?.id != settingsB.usedView?.id
      || false == this.isFixedFiltersEqual(settingsA, settingsB)
      || false == this.isAdjustColumnsEqual(settingsA, settingsB)
      || false == this.isColStateEqual(settingsA, settingsB)
      || false == this.isFilterModelEqual(settingsA, settingsB)
  }

  protected isFixedFiltersEqual(settingsA: DataGridSetting, settingsB: DataGridSetting) {
    return settingsA.fixedFilters == settingsB.fixedFilters;
  }

  protected isAdjustColumnsEqual(settingsA: DataGridSetting, settingsB: DataGridSetting) {
    return settingsA.adjustColumns == settingsB.adjustColumns;
  }

  protected isColStateEqual(settingsA: DataGridSetting, settingsB: DataGridSetting) {
    return this.arrayOfObjectsMatch(settingsA.colsState ?? [], settingsB.colsState ?? []);
  }

  protected isFilterModelEqual(settingsA: DataGridSetting, settingsB: DataGridSetting) {
    return JSON.stringify(settingsA.filterModel ?? {}) == JSON.stringify(settingsB.filterModel ?? {});
  }

  protected arrayOfObjectsMatch(arr1: Array<AnyObject>, arr2: Array<AnyObject>, excludedProperties?: Array<string>) {
    return (arr1.length === arr2.length) && arr1.every((val, index) => {
      for (let key of Object.keys(val)) {
        if ((excludedProperties == null || !excludedProperties.includes(key)) && val[key] !== arr2[index][key]) {
          return false;
        }
      }
      return true;
    });
  }

  public getDataGridColumns() {
    return null;
  }

  public headerCellRendererComp() {
    var comp = {
      init: () => { },
      getGui: (p: any) => {
        return this.headerCellRendererSelectAll();
      },
      destroy: () => { }
    };
    var p = new Promise(() => comp);
    return p;
  }

  protected isSelectedChanged(entity: T) {
    if (entity != null && (entity as any).isSelected && false == this.isSelectable(entity)) {
      (entity as any).isSelected = false;
    }
  }

  protected isSelectable(entity: T): boolean {
    return true;
  }

  //TODO Change Checkbox
  public headerCellRendererSelectAll(): HTMLElement {
    // let newChk = document.createElement('div');
    let eGui = document.createElement('div');
    eGui.style.width = '100%';
    eGui.style.height = '100%';
    let content: string = `<span class="select-all-container"><ui-checkbox if.bind="canSelectAll" checked.bind="selectAll"></ui-checkbox></span>`;
    let html: string = `<template>${content}</template>`;

    let viewCompiler = Container.instance.get(ViewCompiler);
    let viewResources = Container.instance.get(ViewResources);
    let viewFactory = viewCompiler.compile(html, viewResources);
    let view = viewFactory.create(Container.instance);
    let viewSlot = new ViewSlot(eGui, true);
    viewSlot.add(view);
    view.bind(this, createOverrideContext(this));
    viewSlot.attached();
    return eGui;
  }

  protected getSelectableRows(): T[] {
    let rows = [];
    this.gridOptions?.api?.forEachNode((rowNode) => {
      if (rowNode.data != null && this.isSelectable(rowNode.data)) {
        rows.push(rowNode.data);
      }
    });
    return rows;
  }

  private actualizeCheckboxes() {
    // Cocher/décocher les checkbox de la grille suivant l'état de la checkbox du header
    if (this.allDataLoaded()) {
      if (this.selectAll) {
        this.getSelectableRows().forEach(entry => (entry as any).isSelected = this.selectAll);
      }
      else {
        let rowNotSelected = this.getSelectableRows().find(x => !(x as any).isSelected);
        if (rowNotSelected != null) {
          return;
        }
        else {
          this.getSelectableRows().forEach(entry => (entry as any).isSelected = false);
        }
      }
      this.selectedEntitiesCount = this.selectedEntities.length;

      // var chk: HTMLInputElement = (<HTMLInputElement>document.getElementById('selectAllCheckbox-' + this.idSuffix));      
      // if (!chk) return;
      // this.getSelectableRows().forEach(entry => (entry as any).isSelected = chk.checked);
    }
  }

  private allDataLoaded() {
    // Renvoit TRUE si toutes les données de la grille ont été chargées côté client
    var infiniteRowCount = this.gridOptions.api && this.gridOptions.api.getInfiniteRowCount();
    var cacheBlockState: any = this.gridOptions.api && this.gridOptions.api.getCacheBlockState();
    var cptr: number = 0;
    if (cacheBlockState == null) {
      return false;
    }

    for (var i = 0; i < Object.keys(cacheBlockState).length; i++)
      if (cacheBlockState[i] && cacheBlockState[i].pageStatus == 'loaded')
        cptr = cptr + (cacheBlockState[i].endRow - cacheBlockState[i].startRow);

    return infiniteRowCount <= cptr;
  }

  public async initAgGridView() {
    if (this.isViewsEnabled && this.router.currentInstruction.config.moduleId != null) {
      this.agGridViewList = new Array<AgGridView>();
      let defaultView = await this.gridViewService.getDefaultViewFromModuleId(this.router.currentInstruction.config.moduleId);
      if (defaultView == null) {
        defaultView = await this.gridViewService.createEntity({
          id: -100,
          name: this.i18n.tr("general.default", { ns: "common" }), moduleId: this.router.currentInstruction.config.moduleId,
          fixedFilters: false, adjustColumns: this.config.agGridConfig.defaultAdjustColumns, columnState: JSON.stringify(this.gridOptions.columnApi.getColumnState()),
          filterModel: null, sortModel: null, visibility: "System", author: null, authorId: this.authService.currentUser.id,
          isDefaultView: true, createdBy: null, createdById: null, createdTime: null, updatedBy: null, updatedById: null,
          updatedTime: null, entityAspect: null, entityType: null, hide: false, used: false
        });
        defaultView.entityAspect.setDetached();
      }
      this.agGridViewList.push(defaultView);
    }
  }

  public resetFilters() {
    var savedDs = (<any>this.gridOptions.api).rowModel.datasource;
    this.gridOptions.api.setDatasource(null);
    this.gridOptions.api.setFilterModel(null);
    this.gridOptions.columnApi.applyColumnState({
      defaultState: {
        sort: null
      }
    });
    this.gridOptions.api.setDatasource(savedDs);
  }

  //#region Projection
  private applySelect(colDefs: ColDef[]) {
    this.service.gridDataSource.selectPropertyPaths = this.getSelectPropertyPaths(colDefs);
    this.service.gridDataSource.onSuccessGetRowsCallback = async (result) => {
      let serviceExpands = new Array<ServiceExpand>();
      let successResult = Object
        .entries(result)
        .reduce((object, [key, value]) => {
          let obj = {};
          object[key] = obj;
          let properties = Object.keys(value);
          for (let indexProperty = 0; indexProperty < properties.length; indexProperty++) {
            const property = properties[indexProperty];
            let propertyPaths = property.split("_").map((propertyPath) => {
              return CamelCase.toCamelCase(propertyPath);
            });
            let colDef = colDefs.find(x => x.field.startsWith(propertyPaths.join(".")));
            let tempObj = obj;
            for (let index = 0; index < propertyPaths.length; index++) {
              const propertyPath = propertyPaths[index];
              if (!tempObj.hasOwnProperty("entityAspect")) {
                if (tempObj[propertyPath] == undefined) {
                  if (index == propertyPaths.length - 1) {
                    tempObj[propertyPath] = value[property];
                    if (colDef != null) {
                      if (isString(value[property])) {
                        tempObj[propertyPath] = this.convertFromString(value[property], colDef);
                      }
                      else if (isObject(tempObj[propertyPath])) {
                        if (colDef.type == FieldType.ManyToMany) {
                          this.buildArrayForManyToManyExpand(serviceExpands, tempObj, propertyPath, colDef);
                        }
                        else {
                          this.convertFromObject(tempObj[propertyPath], colDef);
                        }
                      }
                    }
                  }
                  else {
                    tempObj[propertyPath] = {};
                  }
                }
                tempObj = tempObj[propertyPath];
              }
            }
          }
          return object;
        }, {}) as any;

      await this.fetchServiceExpands(serviceExpands);

      return successResult;
    }
  }

  private getSelectPropertyPaths(colDefs: Array<ColDef>) {
    let selectPaths = new Array<string>("id");
    for (const colDef of colDefs) {
      if (colDef.hasOwnProperty("field") && colDef.type != null && colDef.type != FieldType.Base) {
        if (colDef.type != FieldType.ManyToMany) {
          let fieldName = colDef.field;
          if (fieldName.includes("._translation")) {
            fieldName = fieldName.replace("._translation", "");
          }
          selectPaths.push(fieldName);
          if (colDef.type == FieldType.Enumeration) {
            let fieldPaths: Array<string> = fieldName.split('.');
            if (fieldPaths.length > 1) {
              let enumerationPath = fieldPaths.slice(0, fieldPaths.length - 1).join('.');
              selectPaths.push(enumerationPath + ".textColor", enumerationPath + ".backgroundColor");
            }
          }
        }
        else {
          if (colDef.floatingFilterComponentParams == null) throw Error("Cannot use projection with many-to-many column without specifying floatingFilterComponentParams parameters");

          let fieldPaths: Array<string> = colDef.field.split('.');
          if (fieldPaths.includes("_translation")) {
            fieldPaths.remove("_translation");
          }
          let arrayField = colDef.floatingFilterComponentParams.arrayField ?? fieldPaths[0];
          let arrayIndex = fieldPaths.findIndex(x => x == arrayField);
          let arrayName: Array<string> = fieldPaths.slice(0, arrayIndex + 1);
          selectPaths.push(arrayName.join('.'));
        }
      }
    }
    if (this.additionalSelectPropertyPaths.length > 0) {
      selectPaths.push(...this.additionalSelectPropertyPaths);
    }
    return selectPaths;
  }

  private convertFromString(variable: string, colDef: ColDef) {
    try {
      if (colDef.type == FieldType.Number) {
        return parseFloat(variable);
      }
      else if (colDef.type == FieldType.Date) {
        return moment.utc(variable).local().toDate();
      }
    }
    catch {
      console.warn("Value '" + variable?.toString() + "' cannot be casted into number or date, fall back to string value");
    }
    return variable;
  }

  private convertFromObject(variable: Object, colDef: ColDef) {
    try {
      if (variable.hasOwnProperty("lang1")) {
        this.translatedPropertyHelper.addNecessaryProperties(variable, false);
      }
    }
    catch {
      console.warn(variable?.toString() + " is not a valid translatedProperty");
    }
  }

  private buildArrayForManyToManyExpand(serviceExpands: Array<ServiceExpand>, tempObj: Object, propertyPath: string, colDef: ColDef) {
    if (colDef.floatingFilterComponentParams?.service != null) {
      let foreignKeyName = colDef.field.substring(colDef.field.indexOf(propertyPath + ".") + propertyPath.length + 1);
      foreignKeyName = foreignKeyName.substring(0, foreignKeyName.indexOf("."));
      let foreignKeyPropertyName = foreignKeyName + "Id";
      for (let indexM = 0; indexM < tempObj[propertyPath].length; indexM++) {
        const element = tempObj[propertyPath][indexM];
        if (element[foreignKeyPropertyName] != null) {
          let serviceExpand = serviceExpands.find(x => x.service.entityTypeName == colDef.floatingFilterComponentParams.service.entityTypeName);
          if (serviceExpand == null) {
            serviceExpands.push({ service: colDef.floatingFilterComponentParams.service, neededIds: [{ neededId: element[foreignKeyPropertyName], entities: [element[foreignKeyName]] }] });
          }
          else {
            let neededId = serviceExpand.neededIds.find(x => x.neededId == element[foreignKeyPropertyName]);
            if (neededId == null) {
              serviceExpand.neededIds.push({ neededId: element[foreignKeyPropertyName], entities: [element[foreignKeyName]] });
            }
            else {
              neededId.entities.push(element[foreignKeyName]);
            }
          }
        }
      }
    }
  }

  private async fetchServiceExpands(serviceExpands: Array<ServiceExpand>) {
    for (const serviceExpand of serviceExpands) {
      let neededIds = new Array<EntityExpand>();
      for (const neededId of serviceExpand.neededIds) {
        let result = await serviceExpand.service.getEntityByIdFromLocalCache(neededId.neededId);
        if (result == null) {
          neededIds.push(neededId);
        }
        else {
          for (let index = 0; index < neededId.entities.length; index++) {
            neededId.entities[index] = result;
          }
        }
      }

      if (neededIds.length > 0) {
        let predicates: breeze.Predicate = breeze.Predicate.or(neededIds.map(neededId => new breeze.Predicate("id", breeze.FilterQueryOp.Equals, neededId.neededId)));

        await serviceExpand.service.getEntities(predicates);

        for (const neededId of neededIds) {
          let result = await serviceExpand.service.getEntityByIdFromLocalCache(neededId.neededId);
          for (let index = 0; index < neededId.entities.length; index++) {
            neededId.entities[index] = result;
          }
        }
      }
    }
  }
  //#endregion
}

interface ServiceExpand {
  service: ServiceBase<any>,
  neededIds: Array<EntityExpand>
}

interface EntityExpand {
  neededId: number,
  entities: Array<Object>
}
