/*
 * Copyright (c) 2010, 2025 BSI Business Systems Integration AG
 *
 * This program and the accompanying materials are made
 * available under the terms of the Eclipse Public License 2.0
 * which is available at https://www.eclipse.org/legal/epl-2.0/
 *
 * SPDX-License-Identifier: EPL-2.0
 */
import {
  AdapterData, App, arrays, BooleanColumn, Cell, ChildModelOf, Column, ColumnModel, ColumnUserFilter, defaultValues, Event, Filter, ModelAdapter, NumberColumn, ObjectOrModel, objects, RemoteEvent, RemoteTableOrganizer, scout, Table,
  TableAggregationFunctionChangedEvent, TableAppLinkActionEvent, TableCancelCellEditEvent, TableColumnBackgroundEffectChangedEvent, TableColumnMovedEvent, TableColumnResizedEvent, TableCompleteCellEditEvent, TableDropEvent,
  TableFilterAddedEvent, TableFilterRemovedEvent, TableGroupEvent, TableModel, TablePrepareCellEditEvent, TableReloadEvent, TableRow, TableRowActionEvent, TableRowClickEvent, TableRowModel, TableRowsCheckedEvent, TableRowsExpandedEvent,
  TableRowsSelectedEvent, TableSortEvent, TableUserFilter, ValueField
} from '../index';
import $ from 'jquery';

export class TableAdapter extends ModelAdapter {
  declare widget: Table;

  /** @internal */
  _rebuildingTable: boolean;

  constructor() {
    super();
    this._addRemoteProperties(['contextColumn']);
  }

  protected override _initProperties(model: TableModel) {
    super._initProperties(model);
    model.compactHandler = null; // Disable Scout JS compact handling, will be done on the server
    model.organizer = scout.create(RemoteTableOrganizer); // table is organized on the server in classic mode
  }

  /** @internal */
  override _postCreateWidget() {
    // if a newly created table has already a user-filter defined, we need to fire the filter event after creation
    // because the original event had been fired before the event-handler was registered.
    if (this.widget.hasUserFilter()) {
      this._onWidgetFilter();
    }
  }

  protected _sendRowsSelected(rowIds: string[], debounceSend?: boolean) {
    let eventData = {
      rowIds: rowIds
    };

    // send delayed to avoid a lot of requests while selecting
    // coalesce: only send the latest selection changed event for a field
    this._send('rowsSelected', eventData, {
      delay: debounceSend ? 250 : 0,
      coalesce: function(previous) {
        return this.target === previous.target && this.type === previous.type;
      }
    });
  }

  protected _sendRowClick(rowId: string, mouseButton: number, columnId: string) {
    let data = {
      rowId: rowId,
      columnId: columnId,
      mouseButton: mouseButton
    };
    this._send('rowClick', data);
  }

  protected _onWidgetRowsSelected(event: TableRowsSelectedEvent) {
    let rowIds = this.widget.rowsToIds(this.widget.selectedRows);
    this._sendRowsSelected(rowIds, event.debounce);
  }

  protected _onWidgetRowClick(event: TableRowClickEvent) {
    let columnId: string;
    if (event.column !== undefined) {
      columnId = event.column.id;
    }

    this._sendRowClick(event.row.id, event.mouseButton, columnId);
  }

  protected _onWidgetFilterAdded(event: TableFilterAddedEvent) {
    let filter = event.filter;
    if (!(filter instanceof TableUserFilter) || (filter instanceof ColumnUserFilter && filter.column.guiOnly)) {
      return;
    }
    this._send('filterAdded', filter.createFilterAddedEventData());
  }

  protected _onWidgetFilterRemoved(event: TableFilterRemovedEvent) {
    let filter = event.filter;
    if (!(filter instanceof TableUserFilter) || (filter instanceof ColumnUserFilter && filter.column.guiOnly)) {
      return;
    }
    this._send('filterRemoved', filter.createFilterRemovedEventData());
  }

  protected _onWidgetColumnResized(event: TableColumnResizedEvent) {
    this._sendColumnResized(event.column);
  }

  protected _sendColumnResized(column: Column<any>) {
    if (column.fixedWidth || column.guiOnly || this.widget.autoResizeColumns) {
      return;
    }

    let eventData = {
      columnId: column.id,
      width: column.width
    };

    // send delayed to avoid a lot of requests while resizing
    // coalesce: only send the latest resize event for a column
    this._send('columnResized', eventData, {
      delay: 750,
      coalesce: function(previous) {
        return this.target === previous.target && this.type === previous.type && this.columnId === previous.columnId;
      },
      showBusyIndicator: false
    });
  }

  protected _onWidgetAggregationFunctionChanged(event: TableAggregationFunctionChangedEvent) {
    this._sendAggregationFunctionChanged(event.column);
  }

  protected _sendAggregationFunctionChanged(column: NumberColumn) {
    if (column.guiOnly) {
      return;
    }
    let data = {
      columnId: column.id,
      aggregationFunction: column.aggregationFunction
    };
    this._send('aggregationFunctionChanged', data);
  }

  protected _onWidgetColumnBackgroundEffectChanged(event: TableColumnBackgroundEffectChangedEvent) {
    this._sendColumnBackgroundEffectChanged(event.column);
  }

  protected _sendColumnBackgroundEffectChanged(column: NumberColumn) {
    if (column.guiOnly) {
      return;
    }
    let data = {
      columnId: column.id,
      backgroundEffect: column.backgroundEffect
    };
    this._send('columnBackgroundEffectChanged', data);
  }

  sendColumnOrganizeAction(column: Column<any>, action: 'add' | 'remove' | 'modify') {
    this._send('columnOrganizeAction', {
      action: action,
      columnId: column.id
    });
  }

  protected _onWidgetColumnMoved(event: TableColumnMovedEvent) {
    let index = event.newPos;
    this.widget.columns.forEach((iteratingColumn, i) => {
      // Adjust index if column is only known on the gui
      if (iteratingColumn.guiOnly) {
        index--;
      }
    });
    this._sendColumnMoved(event.column, index);
  }

  protected _sendColumnMoved(column: Column<any>, index: number) {
    if (column.guiOnly) {
      return;
    }
    let data = {
      columnId: column.id,
      index: index
    };
    this._send('columnMoved', data);
  }

  protected _onWidgetPrepareCellEdit(event: TablePrepareCellEditEvent) {
    event.preventDefault();
    this._sendPrepareCellEdit(event.row, event.column);
  }

  protected _sendPrepareCellEdit(row: TableRow, column: Column<any>) {
    if (column.guiOnly) {
      return;
    }
    let data = {
      rowId: row.id,
      columnId: column.id
    };
    this._send('prepareCellEdit', data);
  }

  protected _onWidgetCompleteCellEdit(event: TableCompleteCellEditEvent) {
    event.preventDefault();
    this._sendCompleteCellEdit();
  }

  protected _sendCompleteCellEdit() {
    this._send('completeCellEdit');
  }

  protected _onWidgetCancelCellEdit(event: TableCancelCellEditEvent) {
    event.preventDefault();
    this._sendCancelCellEdit();
  }

  protected _sendCancelCellEdit() {
    this._send('cancelCellEdit');
  }

  protected _onWidgetRowsChecked(event: TableRowsCheckedEvent) {
    this._sendRowsChecked(event.rows);
  }

  protected _sendRowsChecked(rows: TableRow[]) {
    let data = {
      rows: []
    };

    for (let i = 0; i < rows.length; i++) {
      data.rows.push({
        rowId: rows[i].id,
        checked: rows[i].checked
      });
    }

    this._send('rowsChecked', data);
  }

  protected _onWidgetRowsExpanded(event: TableRowsExpandedEvent) {
    this._sendRowsExpanded(event.rows);
  }

  protected _sendRowsExpanded(rows: TableRow[]) {
    let data = {
      rows: rows.map(row => {
        return {
          rowId: row.id,
          expanded: row.expanded
        };
      })
    };
    this._send('rowsExpanded', data);
  }

  protected _onWidgetFilter() {
    let rowIds = this.widget.rowsToIds(this.widget.filteredRows());
    this._sendFilter(rowIds);
  }

  protected _sendFilter(rowIds: string[]) {
    let eventData: { remove?: boolean; rowIds?: string[] } = {};
    if (rowIds.length === this.widget.rows.length) {
      eventData.remove = true;
    } else {
      eventData.rowIds = rowIds;
    }

    // send with timeout, mainly for incremental load of a large table
    // coalesce: only send last event (don't coalesce remove and 'add' events, the UI server needs both)
    this._send('filter', eventData, {
      delay: 250,
      coalesce: function(previous) {
        return this.target === previous.target && this.type === previous.type && this.remove === previous.remove;
      },
      showBusyIndicator: false
    });
  }

  protected _onWidgetSort(event: TableSortEvent) {
    if (event.column.guiOnly) {
      return;
    }
    this._send('sort', {
      columnId: event.column.id,
      sortAscending: event.sortAscending,
      sortingRemoved: event.sortingRemoved,
      multiSort: event.multiSort,
      sortingRequested: event.sortingRequested
    });
  }

  protected _onWidgetGroup(event: TableGroupEvent) {
    if (event.column.guiOnly) {
      return;
    }
    this._send('group', {
      columnId: event.column.id,
      groupAscending: event.groupAscending,
      groupingRemoved: event.groupingRemoved,
      multiGroup: event.multiGroup,
      groupingRequested: event.groupingRequested
    });
  }

  protected _onWidgetRowAction(event: TableRowActionEvent) {
    this._sendRowAction(event.row, event.column);
  }

  protected _sendRowAction(row: TableRow, column: Column<any>) {
    if (column.guiOnly) {
      // Send row action with a real column
      // If there is only one guiOnly column (e.g. CompactColumn), sent column will be null
      column = arrays.find(this.widget.columns, col => !col.guiOnly);
    }
    let columnId = column ? column.id : null;
    this._send('rowAction', {
      rowId: row.id,
      columnId: columnId
    });
  }

  protected _onWidgetAppLinkAction(event: TableAppLinkActionEvent) {
    this._sendAppLinkAction(event.column, event.ref);
  }

  protected _sendAppLinkAction(column: Column<any>, ref: string) {
    this._send('appLinkAction', {
      columnId: column.id,
      ref: ref
    });
  }

  protected _sendContextColumn(contextColumn: Column<any>) {
    if (contextColumn.guiOnly) {
      contextColumn = null;
      this.widget.contextColumn = null;
    }
    let columnId: string = null;
    if (contextColumn) {
      columnId = contextColumn.id;
    }
    this._send('property', {
      contextColumn: columnId
    });
  }

  protected _onWidgetReload(event: TableReloadEvent) {
    let data = {
      reloadReason: event.reloadReason
    };
    this._send('reload', data);
  }

  protected _onWidgetExportToClipboard(event: Event<Table>) {
    this._send('clipboardExport');
    event.preventDefault();
  }

  protected override _onWidgetEvent(event: Event<Table>) {
    if (event.type === 'rowsSelected') {
      this._onWidgetRowsSelected(event as TableRowsSelectedEvent);
    } else if (event.type === 'rowsChecked') {
      this._onWidgetRowsChecked(event as TableRowsCheckedEvent);
    } else if (event.type === 'rowsExpanded') {
      this._onWidgetRowsExpanded(event as TableRowsExpandedEvent);
    } else if (event.type === 'filter') {
      this._onWidgetFilter();
    } else if (event.type === 'sort') {
      this._onWidgetSort(event as TableSortEvent);
    } else if (event.type === 'group') {
      this._onWidgetGroup(event as TableGroupEvent);
    } else if (event.type === 'rowClick') {
      this._onWidgetRowClick(event as TableRowClickEvent);
    } else if (event.type === 'rowAction') {
      this._onWidgetRowAction(event as TableRowActionEvent);
    } else if (event.type === 'prepareCellEdit') {
      this._onWidgetPrepareCellEdit(event as TablePrepareCellEditEvent);
    } else if (event.type === 'completeCellEdit') {
      this._onWidgetCompleteCellEdit(event as TableCompleteCellEditEvent);
    } else if (event.type === 'cancelCellEdit') {
      this._onWidgetCancelCellEdit(event as TableCancelCellEditEvent);
    } else if (event.type === 'appLinkAction') {
      this._onWidgetAppLinkAction(event as TableAppLinkActionEvent);
    } else if (event.type === 'clipboardExport') {
      this._onWidgetExportToClipboard(event);
    } else if (event.type === 'reload') {
      this._onWidgetReload(event as TableReloadEvent);
    } else if (event.type === 'filterAdded') {
      this._onWidgetFilterAdded(event as TableFilterAddedEvent);
    } else if (event.type === 'filterRemoved') {
      this._onWidgetFilterRemoved(event as TableFilterRemovedEvent);
    } else if (event.type === 'columnResized') {
      this._onWidgetColumnResized(event as TableColumnResizedEvent);
    } else if (event.type === 'columnMoved') {
      this._onWidgetColumnMoved(event as TableColumnMovedEvent);
    } else if (event.type === 'columnBackgroundEffectChanged') {
      this._onWidgetColumnBackgroundEffectChanged(event as TableColumnBackgroundEffectChangedEvent);
    } else if (event.type === 'aggregationFunctionChanged') {
      this._onWidgetAggregationFunctionChanged(event as TableAggregationFunctionChangedEvent);
    } else if (event.type === 'drop' && this.widget.dragAndDropHandler) {
      this.widget.dragAndDropHandler.uploadFiles(event as TableDropEvent);
    } else {
      super._onWidgetEvent(event);
    }
  }

  protected _onRowsInserted(rows: ObjectOrModel<TableRow> | ObjectOrModel<TableRow>[]) {
    this.widget.insertRows(rows);
    this._rebuildingTable = false;
  }

  protected _onRowsDeleted(rowIds: string[]) {
    let rows = this.widget.rowsByIds(rowIds);
    this.addFilterForWidgetEventType('rowsSelected');
    this.widget.deleteRows(rows);
  }

  protected _onAllRowsDeleted() {
    this.addFilterForWidgetEventType('rowsSelected');
    this.widget.deleteAllRows();
  }

  protected _onRowsUpdated(rows: TableRow | TableRow[]) {
    this.widget.updateRows(rows);
  }

  protected _onRowsSelected(rowIds: string[]) {
    let rows = this.widget.rowsByIds(rowIds);
    this.addFilterForWidgetEventType('rowsSelected');
    this.widget.selectRows(rows);
  }

  protected _onRowsChecked(rows: TableRowModel[]) {
    let checkedRows: TableRow[] = [],
      uncheckedRows: TableRow[] = [];

    rows.forEach(rowData => {
      let row = this.widget.rowById(rowData.id);
      if (rowData.checked) {
        checkedRows.push(row);
      } else {
        uncheckedRows.push(row);
      }
    });

    this.addFilterForWidgetEventType('rowsChecked');
    this.widget.checkRows(checkedRows, {
      checked: true,
      checkOnlyEnabled: false
    });
    this.widget.uncheckRows(uncheckedRows, {
      checkOnlyEnabled: false
    });
  }

  protected _onRowsExpanded(rows: TableRowModel[]) {
    let expandedRows: TableRow[] = [],
      collapsedRows: TableRow[] = [];
    rows.forEach(rowData => {
      let row = this.widget.rowById(rowData.id);
      if (rowData.expanded) {
        expandedRows.push(row);
      } else {
        collapsedRows.push(row);
      }
    });
    this.addFilterForWidgetEventType('rowsExpanded');

    this.widget.expandRows(expandedRows);
    this.widget.collapseRows(collapsedRows);
  }

  protected _onRowOrderChanged(rowIds: string[]) {
    let rows = this.widget.rowsByIds(rowIds);
    this.widget.updateRowOrder(rows);
  }

  protected _onColumnStructureChanged(columns: Column<any>[]) {
    this._rebuildingTable = true;
    this.widget.updateColumnStructure(columns);
  }

  protected _onColumnOrderChanged(columnIds: string[]) {
    let columns = this.widget.columnsByIds(columnIds);
    this.widget.updateColumnOrder(columns);
  }

  protected _onColumnHeadersUpdated(columns: Column<any>[]) {
    columns.forEach(column => defaultValues.applyTo(column));
    this.widget.updateColumnHeaders(columns);

    if (this.widget.tileMode && this.widget.tableTileGridMediator) {
      // grouping might have changed, trigger re-init of the groups on the tileGrid in tileMode
      this.widget.tableTileGridMediator._onTableGroup();
      // removing of a group column doesn't cause a rowOrderChange, nonetheless aggregation columns might need to be removed.
      this.widget.updateRowOrder(this.widget.rows);
    }
  }

  protected _onStartCellEdit(columnId: string, rowId: string, fieldId: string) {
    let column = this.widget.columnById(columnId),
      row = this.widget.rowById(rowId),
      field = this.session.getOrCreateWidget(fieldId, this.widget) as ValueField<any>;

    this.widget.startCellEdit(column, row, field);
  }

  protected _onEndCellEdit(fieldId: string) {
    let field = this.session.getModelAdapter(fieldId);
    if (!field) {
      throw new Error('Field adapter could not be resolved. Id: ' + fieldId);
    }
    this.widget.endCellEdit(field.widget as ValueField<any>);
  }

  protected _onRequestFocus() {
    this.widget.focus();
  }

  protected _onScrollToSelection() {
    this.widget.revealSelection();
  }

  protected _onColumnBackgroundEffectChanged(event: RemoteEvent) {
    event.eventParts.forEach(function(eventPart) {
      let column = this.widget.columnById(eventPart.columnId),
        backgroundEffect = eventPart.backgroundEffect;

      this.addFilterForWidgetEvent(widgetEvent => {
        return (widgetEvent.type === 'columnBackgroundEffectChanged' &&
          widgetEvent.column.id === column.id &&
          widgetEvent.column.backgroundEffect === backgroundEffect);
      });

      column.setBackgroundEffect(backgroundEffect);
    }, this);
  }

  protected _onRequestFocusInCell(event: RemoteEvent) {
    let row = this.widget.rowById(event.rowId),
      column = this.widget.columnById(event.columnId);
    this.widget.focusCell(column, row);
  }

  protected _onAggregationFunctionChanged(event: RemoteEvent) {
    let columns = [],
      functions = [];

    event.eventParts.forEach(function(eventPart) {
      let func = eventPart.aggregationFunction,
        column = this.widget.columnById(eventPart.columnId);

      this.addFilterForWidgetEvent(widgetEvent => {
        return (widgetEvent.type === 'aggregationFunctionChanged' &&
          widgetEvent.column.id === column.id &&
          widgetEvent.column.aggregationFunction === func);
      });

      columns.push(column);
      functions.push(func);
    }, this);

    this.widget.changeAggregations(columns, functions);
  }

  protected _onFiltersChanged(filters: (ObjectOrModel<TableUserFilter> | Filter<TableRow>)[]) {
    this.addFilterForWidgetEventType('filterAdded');
    this.addFilterForWidgetEventType('filterRemoved');

    this.widget.setFilters(filters);
    // do not re-filter while the table is being rebuilt (because column.index in filter and row.cells may be inconsistent)
    if (!this._rebuildingTable) {
      this.widget.filter();
    }
  }

  override onModelAction(event: RemoteEvent) {
    if (event.type === 'rowsInserted') {
      this._onRowsInserted(event.rows);
    } else if (event.type === 'rowsDeleted') {
      this._onRowsDeleted(event.rowIds);
    } else if (event.type === 'allRowsDeleted') {
      this._onAllRowsDeleted();
    } else if (event.type === 'rowsSelected') {
      this._onRowsSelected(event.rowIds);
    } else if (event.type === 'rowOrderChanged') {
      this._onRowOrderChanged(event.rowIds);
    } else if (event.type === 'rowsUpdated') {
      this._onRowsUpdated(event.rows);
    } else if (event.type === 'filtersChanged') {
      this._onFiltersChanged(event.filters);
    } else if (event.type === 'rowsChecked') {
      this._onRowsChecked(event.rows);
    } else if (event.type === 'rowsExpanded') {
      this._onRowsExpanded(event.rows);
    } else if (event.type === 'columnStructureChanged') {
      this._onColumnStructureChanged(event.columns);
    } else if (event.type === 'columnOrderChanged') {
      this._onColumnOrderChanged(event.columnIds);
    } else if (event.type === 'columnHeadersUpdated') {
      this._onColumnHeadersUpdated(event.columns);
    } else if (event.type === 'startCellEdit') {
      this._onStartCellEdit(event.columnId, event.rowId, event.fieldId);
    } else if (event.type === 'endCellEdit') {
      this._onEndCellEdit(event.fieldId);
    } else if (event.type === 'requestFocus') {
      this._onRequestFocus();
    } else if (event.type === 'scrollToSelection') {
      this._onScrollToSelection();
    } else if (event.type === 'aggregationFunctionChanged') {
      this._onAggregationFunctionChanged(event);
    } else if (event.type === 'columnBackgroundEffectChanged') {
      this._onColumnBackgroundEffectChanged(event);
    } else if (event.type === 'requestFocusInCell') {
      this._onRequestFocusInCell(event);
    } else {
      super.onModelAction(event);
    }
  }

  override exportAdapterData(adapterData: AdapterData): AdapterData {
    adapterData = super.exportAdapterData(adapterData);
    delete adapterData.selectedRows;
    adapterData.rows = [];
    adapterData.columns.forEach(column => {
      delete column.classId;
      delete column.modelClass;
    });
    return adapterData;
  }

  protected _initRowModel(rowModel?: TableRowModel): ChildModelOf<TableRow> {
    let model = (rowModel || {}) as ChildModelOf<TableRow>;
    model.objectType = scout.nvl(model.objectType, 'TableRow');
    defaultValues.applyTo(model);
    return model;
  }

  protected static _createRowRemote(this: Table & { modelAdapter: TableAdapter; _createRowOrig }, rowModel: TableRowModel): TableRow {
    if (this.modelAdapter) {
      rowModel = this.modelAdapter._initRowModel(rowModel);
    }
    return this._createRowOrig(rowModel);
  }

  /**
   * Static method to modify the prototype of Table.
   */
  static modifyTablePrototype() {
    if (!App.get().remote) {
      return;
    }

    objects.replacePrototypeFunction(Table, '_createRow', TableAdapter._createRowRemote, true);

    // _sortWhileInit
    objects.replacePrototypeFunction(Table, '_sortWhileInit', function(this: Table & { _sortWhileInitOrig }) {
      if (this.modelAdapter) {
        // Scout classic: not necessary to sort in init as the rows are already sorted on the Java UI. Only apply grouping here.
        this._group();
      } else {
        this._sortWhileInitOrig();
      }
    }, true);

    // _sortAfterInsert
    objects.replacePrototypeFunction(Table, '_sortAfterInsert', function(this: Table & { _sortAfterInsertOrig }, wasEmpty: boolean) {
      if (this.modelAdapter) {
        // There will only be a row order changed event if table was not empty.
        // If it was empty, there will be NO row order changed event (tableEventBuffer) -> inserted rows are already in correct order -> no sort necessary but group is
        if (wasEmpty) {
          this._group();
        }
      } else {
        this._sortAfterInsertOrig(wasEmpty);
      }
    }, true);

    // _sortAfterUpdate
    objects.replacePrototypeFunction(Table, '_sortAfterUpdate', function(this: Table & { _sortAfterUpdateOrig }) {
      if (this.modelAdapter) {
        this._group();
      } else {
        this._sortAfterUpdateOrig();
      }
    }, true);

    // uiSortPossible
    objects.replacePrototypeFunction(Table, '_isSortingPossible', function(this: Table & { uiSortPossible: boolean; _isSortingPossibleOrig }, sortColumns: Column<any>[]) {
      if (this.modelAdapter) {
        // In a JS only app the flag 'uiSortPossible' is never set and thus defaults to true. Additionally, we check if each column can install
        // its comparator used to sort. If installation failed for some reason, sorting is not possible. In a remote app the server sets the
        // 'uiSortPossible' flag, which decides if the column must be sorted by the server or can be sorted by the client.
        let uiSortPossible = scout.nvl(this.uiSortPossible, true);
        return uiSortPossible && this._isSortingPossibleOrig(sortColumns);
      }
      return this._isSortingPossibleOrig(sortColumns);
    }, true);

    // sort
    objects.replacePrototypeFunction(Table, 'sort', function(this: Table & { sortOrig }, column: Column<any>, direction?: 'asc' | 'desc', multiSort?: boolean, remove?: boolean) {
      if (this.modelAdapter && column.guiOnly) {
        return;
      }
      this.sortOrig(column, direction, multiSort, remove);
    }, true);

    // no js default tileTableHeader in classic mode
    objects.replacePrototypeFunction(Table, '_createTileTableHeader', function(this: Table & { _createTileTableHeaderOrig }) {
      if (this.modelAdapter) {
        return null;
      }
      return this._createTileTableHeaderOrig();
    }, true);

    // not used in classic mode since tiles are created by the server
    objects.replacePrototypeFunction(Table, 'createTiles', function(this: Table & { createTilesOrig: typeof Table.prototype.createTiles }, rows: TableRow[]) {
      if (this.modelAdapter) {
        return null;
      }
      return this.createTilesOrig(rows);
    }, true);
  }

  static modifyColumnPrototype() {
    if (!App.get().remote) {
      return;
    }

    // init
    objects.replacePrototypeFunction(Column, 'init', function(this: Column & { initOrig }, model: ColumnModel<any>) {
      if (model.parent && model.parent.modelAdapter && !model.guiOnly) {
        // Fill in the missing default values only in remote case, don't do it JS case to not accidentally set undefined properties (e.g. uiSortEnabled)
        model = $.extend({}, model);
        defaultValues.applyTo(model);
      }
      this.initOrig(model);
    }, true);

    // _ensureCell
    objects.replacePrototypeFunction(Column, '_ensureCell', function(this: Column & { _ensureCellOrig; _ensureValue }, vararg: any) {
      if (this.table.modelAdapter) {
        // Note: we do almost the same thing as in _ensureCellOrig, the difference is that
        // we treat a plain object always as cell-model and we always must apply defaultValues
        // to this cell model. In the JS only case a plain-object has no special meaning and
        // can be used as cell-value in the same way as a scalar value. Also, we must not apply
        // defaultValues in JS only case, because it would destroy the 'undefined' state of the
        // cell properties, which is required because the Column checks, whether it should apply
        // defaults from the Column instance to a cell, or use the values from the cell.
        let model;
        if (objects.isObject(vararg)) {
          model = vararg;
          model.value = this._ensureValue(model.value);
          // Parse the value if a text but no value is provided. The server does only set the text if value and text are equal.
          // It is also necessary for custom columns which don't have a UI representation and never send the value.
          // Do not parse the value if there is an error status.
          // If editing fails, the display text will be the user input, the value unchanged, and the server will set the error status.
          if (model.text && model.value === undefined && !model.errorStatus) {
            model.value = this._ensureValue(model.text);
          }
          // use null instead of undefined
          if (model.value === undefined) {
            model.value = null;
          }
        } else {
          model = {
            value: this._ensureValue(vararg)
          };
        }
        defaultValues.applyTo(model, 'Cell');
        return scout.create(Cell, model);
      }
      return this._ensureCellOrig(vararg);
    }, true);

    // uiSortPossible
    objects.replacePrototypeFunction(Column, 'isSortingPossible', function(this: Column & { uiSortPossible: boolean; isSortingPossibleOrig: typeof Column.prototype.isSortingPossible }) {
      if (this.table.modelAdapter) {
        // Returns whether this column can be used to sort on the client side. In a JS only app the flag 'uiSortPossible'
        // is never set and defaults to true. As a side effect of this function a comparator is installed.
        // The comparator returns false if it could not be installed which means sorting should be delegated to server (e.g. collator is not available).
        // In a remote app the server sets the 'uiSortPossible' flag, which decides if the column must be sorted by the
        // server or can be sorted by the client.
        let uiSortPossible = scout.nvl(this.uiSortPossible, true);
        return uiSortPossible && this.installComparator();
      }
      return this.isSortingPossibleOrig();
    }, true);
  }

  static modifyBooleanColumnPrototype() {
    if (!App.get().remote) {
      return;
    }

    // _toggleCellValue
    objects.replacePrototypeFunction(BooleanColumn, '_toggleCellValue', function(this: BooleanColumn & { _toggleCellValueOrig }, row: TableRow, cell: Cell<boolean>) {
      if (this.table.modelAdapter) {
        // NOP - do nothing, since server will handle the click, see Java AbstractTable#interceptRowClickSingleObserver
      } else {
        this._toggleCellValueOrig(row, cell);
      }
    }, true);
  }
}

App.addListener('bootstrap', TableAdapter.modifyTablePrototype);
App.addListener('bootstrap', TableAdapter.modifyColumnPrototype);
App.addListener('bootstrap', TableAdapter.modifyBooleanColumnPrototype);
