import { BaseEntity, ErrorCallBack, BaseEntityCallBack } from './baseentity';
import { BaseEntityWrapper } from './baseentity.wrapper';
import { Subscription, interval, Observable, Subject, forkJoin, of } from 'rxjs';
import { BaseFilter } from './baseentityfilter';
import { RepositoryService } from '../../services/http/repository.service';
import { UINotifierService } from '../../services/notification/uinotifier.service';
import { cnst_CANCEL_ERROR, EN_EntityBeforeSaveCallBackResults, EN_EntityEditingStates } from '../../consts';
import { getErrorMessageAsText, isEqual, now } from '../../utils';
import { AppLogsService } from '../../services/applogs.service';
import { ResultSubject } from '../resultsubject';
import * as keycode from '@angular/cdk/keycodes';
import { HenselTranslateService } from '../../services/localization/hensel-translate.service';
import { inject } from '@angular/core';

export class BaseEntityListWrapper<T extends BaseEntity> {
  public static delayDoItemSelect: number = 500; //item is selected if the last ..ms doSelect was not called
  public static delayDoItemFocus: number = 500; //item is focused if the last ..ms doFocus was not called
  public static dalayFocusLastItem: number = 500; //delay after last item of the list is selected in the grid component
  public static dalayFocusFirstItem: number = 1250; //delay after last item of the list is selected in the grid component, use it to wait the finish of grid sorting
  public focusedEntityShadowed: BaseEntityWrapper<T>;
  public reloadAfterItemUpdate: boolean = false;
  public alwaysFocused: boolean = true; // approves that if the list is not empty it has always focused row (after delete rows or reloading), after loading the focused is not changed
  public afterLoadFocusFirstItem: boolean = false; // approves that after reloading the first row always has focus
  public batchInsert: boolean = true;
  public focusedItemGridIndex: number = -1;
  public useFocusedGridIndex: boolean = true;
  public focusedCallBack: BaseEntityCallBack;
  public focusedRoutingCallBack: BaseEntityCallBack;
  public savedCallBack: VoidFunction;
  public confirmationsOn: boolean = false;
  public dontShowErrors: boolean = true;
  public beforeSaveCallBack: (entity: BaseEntity[]) => Promise<EN_EntityBeforeSaveCallBackResults>; //callback to be called before save, returns 0-all ok, continue saving, 1-all ok, break further saving, 2 - error, ignore coninue
  public afterInsertCallBack: (entity: BaseEntity, callback?: BaseEntityCallBack) => void;
  public isConnectedWithSortedGrid: boolean = true;
  public connectedGrid: any;
  public deletedItems: T[] = [];
  public shouldBeNotEmpty: boolean = false;
  public entityListLoaded$: Subject<void> = new Subject();
  public translate: HenselTranslateService;

  private _isLoading: boolean = false;
  public updateItemIfWasNotFoundInList: boolean = false;

  public get isLoading(): boolean {
    return this._isLoading;
  }
  public set isLoading(value: boolean) {
    const wasLoading = this._isLoading;
    this._isLoading = value;
    if (wasLoading && !value) this.entityListLoaded$.next();
  }


  private _hierarchyLevel: number = 0;
  public get hierarchyLevel(): number {
    return this._hierarchyLevel;
  }
  public set hierarchyLevel(value: number) {
    this._hierarchyLevel = value;
    if (this.focusedEntityShadowed) this.focusedEntityShadowed.hierarchyLevel = value;
  }

  protected _focusedTimer: Subscription = null;
  protected _selectedTimer: Subscription = null;
  protected _selectedItemIds: number[] = [];
  protected _focusedItemId: number = 0;
  protected _items: T[] = [];

  private _reloadAnyway: boolean = false;
  private readonly lastElement = -1;
  private loginSubscriptionSave: Subscription;
  private loginSubscriptionLoad: Subscription;

  protected appLogsService: AppLogsService;
  protected repositoryService: RepositoryService;
  protected uiNotificationsService: UINotifierService;

  constructor(
    protected TEntity: new (src?: Partial<BaseEntity>) => T,
    public _listName: string,
    public routing: string, //controller route
    public filter?: BaseFilter,
    appLogsService?: AppLogsService,
    repositoryService?: RepositoryService,
    uiNotificationsService?: UINotifierService,
  ) {
    this.appLogsService = appLogsService ?? inject(AppLogsService);
    this.repositoryService = repositoryService ?? inject(RepositoryService);
    this.uiNotificationsService = uiNotificationsService ?? inject(UINotifierService);
    this.translate = this.appLogsService.translate;
  }

  public get items(): T[] { return this._items; }


  public set items(newItems: T[]) {
    this._items = newItems;
    this.deletedItems = [];
    // this.clearSelected(); //is done indirectly in approveFocusedSelectedItems
    this.approveFocusedSelectedItems();
  }

  get selectedItemIds(): number[] {
    return this._selectedItemIds;
  }


  set selectedItemIds(list: number[]) {
    //    if (!(this._selectedItemsLast.length === list.length && list.every(_ => this._selectedItemsLast.find(vl => vl === _) !== undefined))) {
    //    this._selectedItemsLast = Object.assign([], list);

    this._selectedItemIds = list;
    //Hack to avoid multicall of selected event

    if (this._selectedTimer) this._selectedTimer.unsubscribe();
    this._selectedTimer = interval(BaseEntityListWrapper.delayDoItemSelect).subscribe(() => {
      this._selectedTimer?.unsubscribe();
      this.doSelect(list);
    });
  }

  get focusedItemId(): number { return this._focusedItemId; }
  set focusedItemId(focusedId: number) {
    if (Array.isArray(focusedId)) focusedId = focusedId.length > 0 ? focusedId[0] : 0;
    if (!focusedId || (typeof focusedId !== 'number')) focusedId = 0; //avoid to fire if focusedId is undefined or null or NaN, which we can get from the grid component
    if (this._focusedItemId === focusedId && !this._reloadAnyway && focusedId /* important for details.enablededit() at the start*/) return;
    this._focusedItemId = focusedId;

    if (this._focusedTimer) this._focusedTimer.unsubscribe();
    this._focusedTimer = interval(BaseEntityListWrapper.delayDoItemFocus).subscribe(() => {
      this._focusedTimer?.unsubscribe();
      this.doFocus(focusedId);
    });
  }

  public get focusedItem(): T { return this.focusedItemId ? this.getItemById(this.focusedItemId) : null; }


  public get changed(): boolean {
    return (this.deletedItems.length > 0) || this.items.some((it: BaseEntity) => it.entityChanged);
  }


  public get isValidInput(): boolean {
    return (!this.shouldBeNotEmpty || this.items.length > 0) && this.items.every((it: BaseEntity) => it.isValidInput);
  }


  protected approveFocusedSelectedItems(newId: number = 0) {
    const _focusedItem: T = this.focusedItem;
    //    setTimeout(() => {
    this._reloadAnyway = true; //to call focused event for details anyway (even focusedItemId was the same)
    if (!_focusedItem) {
      if (newId !== 0) this.focusedItemId = newId;
      else if (this.alwaysFocused) this.focusFirstItem();
      else this.focusedItemId = 0;
    } else {
      // this._reloadAnyway = true;
      this.focusedItemId = this.focusedItemId; //reload focused entity anyway
    }
    if (this.items.length === 0) this.clearSelected();
    else this.selectedItemIds = this.selectedItemIds.filter(id => this.getItemById(id) !== null);
    //  }, 0/*cnstChangedWaitInterval*/);
  }


  public focusFirstItem(_loadedCallback?: (entity: T[]) => void, _interval: number = BaseEntityListWrapper.dalayFocusFirstItem) {
    const lastFocusItemIdIndex = this.useFocusedGridIndex ? this.focusedItemGridIndex : this.focusedItemId;
    if (this.useFocusedGridIndex) _interval = 0; //if the gridindex is using is used - does not matter the grid sorting
    setTimeout(() => { //to set after grid's sorting
      // to fix - during setting of focusedItemGridIndex, focusedItemId is changed and causes "check after change error"
      const curFocusItemIdIndex = this.useFocusedGridIndex ? this.focusedItemGridIndex : this.focusedItemId;
      // this.appLogsService.dlog('--- Last Focused Index/Id', this._listName, lastFocusItemIdIndex, curFocusItemIdIndex);
      if (this.items && this.items.length > 0) {
        if (lastFocusItemIdIndex === curFocusItemIdIndex) { //grid focus was not explicetely changed
          if (this.useFocusedGridIndex) this.focusedItemGridIndex = 0; // to be aware of grid sorting
          else this.focusedItemId = this.items[0].entityId;
        }
      } else this.focusedItemId = 0;
      if (_loadedCallback) _loadedCallback(this.items);
    }, _interval);
  }


  public focusLastItem(_loadedCallback?: (entity: T[]) => void, _interval: number = BaseEntityListWrapper.dalayFocusLastItem) {
    const lastFocusItemIdIndex = this.useFocusedGridIndex ? this.focusedItemGridIndex : this.focusedItemId;
    if (this.useFocusedGridIndex) _interval = 0; //if the gridindex is using is used - does not matter the grid sorting
    setTimeout(() => { //to set after grid's sorting
      const curFocusItemIdIndex = this.useFocusedGridIndex ? this.focusedItemGridIndex : this.focusedItemId;
      // this.appLogsService.dlog('--- Last Focused Index/Id', this._listName, lastFocusItemIdIndex, curFocusItemIdIndex);
      if (this.items && this.items.length > 0) {
        if (lastFocusItemIdIndex === curFocusItemIdIndex) { //grid focus was not explicetely changed
          if (this.useFocusedGridIndex) this.focusedItemGridIndex = this.items.length - 1; // to be aware of grid sorting
          else this.focusedItemId = this.items[this.items.length - 1].entityId;
        }
      } else this.focusedItemId = 0;
      if (_loadedCallback) _loadedCallback(this.items);
    }, _interval);
  }


  public gridUpDownProcessing(e: any) {
    switch (e.event.keyCode) {
      case keycode.HOME:
        this.focusFirstItem(null, 100);
        e.event.stopPropagation();
        break;
      case keycode.END:
        this.focusLastItem(null, 100);
        e.event.stopPropagation();
        break;
    }
  }

  public getItemById(id: number): T { return this.items ? this.items.find(it => it.entityId === id) : null; }


  public getItemIndex(entity: T | number): number {
    if ((typeof entity) === 'number') {
      return this.items.findIndex((it) => (it.entityId === entity));
    } else if (entity instanceof BaseEntity) {
      return this.items.findIndex((it) => it === entity || (
        entity?.entityId && (
          it?.entityId === entity?.entityId
          || entity.insertWithIds() && -it?.entityId === entity?.entityId //in the case of insertWithIds entityId = -entityId after saving
        )
      ));
    } else return this.items.findIndex((it: any) => it === entity);
  }


  public get focusedItemIndex(): number {
    return this.getItemIndex(this.focusedItem);
  }


  public isDeleted(entity: T, respawn: boolean = false): boolean {
    const indexInDeleted: number = this.deletedItems.indexOf(entity);
    if (respawn && indexInDeleted > -1) this.deletedItems.splice(indexInDeleted, 1);
    return indexInDeleted > -1;
  }


  setTriggerItem(entity: T, itemOn: boolean) {
    if (!itemOn) {
      if (!entity.isNew()) this.deletedItems.push(entity); //mark for deleting if already saved, bot leave in the main list
      entity.entityChanged = false; //set to be not saved from main list
    } else {
      entity.entityChanged = true; //mark to be saved
      this.isDeleted(entity, true); //remove from list of items to delete if was there
    }
  }


  public clearSelected() { this.selectedItemIds = []; }


  public clearFocused() { this.focusedItemId = 0; }


  public clear() {
    this.items = [];
    // this._items = [];
    // this.clearSelected();
    // this.clearFocused();
    // if (this.focusedEntityShadowed) this.focusedEntityShadowed.clear();
  }


  public createFocusedShadowEntity(defaultEditMode?: EN_EntityEditingStates): BaseEntityWrapper<T> {
    this.focusedEntityShadowed = new BaseEntityWrapper<T>(this.TEntity, defaultEditMode);
    this.focusedEntityShadowed.entityList = this;
    this.focusedEntityShadowed.hierarchyLevel = this.hierarchyLevel;
    return this.focusedEntityShadowed;
  }


  public load(_loadedCallback?: (entity: T[]) => void, _errorCallback?: ErrorCallBack, _completeCallback?: () => void): boolean //
  {
    if (this.filter && this.filter.useObjectFilter) {
      return this.loadWithObjFilter(_loadedCallback, _errorCallback, _completeCallback);
    } else this.loadWithURLFilter(_loadedCallback, _errorCallback, _completeCallback);

  }


  public loadWithObjFilter(_loadedCallback?: (entity: T[]) => void, _errorCallback?: ErrorCallBack, _completeCallback?: () => void): boolean //
  {
    return this._load(this.repositoryService.getDataByObjFilter<T[]>(this.routing + (this.filter.filterPrefix ? '/' + this.filter.filterPrefix : ''), this.filter.getFilter()), _loadedCallback, _errorCallback, _completeCallback);
  }


  public loadWithURLFilter(_loadedCallback?: (entity: T[]) => void, _errorCallback?: ErrorCallBack, _completeCallback?: () => void): boolean //
  {
    return this._load(this.repositoryService.getDataByURLFilter<T[]>(this.routing, this.filter ? this.filter.getFilterURL() : ''), _loadedCallback, _errorCallback, _completeCallback);
  }



  protected _load(loadList$: Observable<T[]>, _loadedCallback?: (entity: T[]) => void, _errorCallback?: ErrorCallBack, _completeCallback?: () => void): boolean {
    let msgID: number;

    if (!this.loginSubscriptionLoad && this.hierarchyLevel === 0) {
      this.loginSubscriptionLoad = this.repositoryService.authService.loginAction$.subscribe((resultObject) => {
        if (resultObject.result) { //try one time more
          this._load(loadList$, _loadedCallback, _errorCallback, _completeCallback);
        } else this.unsubscribe(this.loginSubscriptionLoad);
      });
    } else {
      if (this.loginSubscriptionLoad) this.unsubscribe(this.loginSubscriptionLoad); // try only one time more
    }

    this.isLoading = true;
    if (this.uiNotificationsService) msgID = this.uiNotificationsService.showMessage(this.translate.instant('core.msg.loading', { value: this.translate.instant(this._listName) }));
    const filter4Call: BaseFilter = this.filter ? Object.assign({}, this.filter) : null;
    let afterLoadFocusFirstItem = this.afterLoadFocusFirstItem;
    loadList$.subscribe(
      (entities: T[]) => {
        try {
          this.unsubscribe(this.loginSubscriptionLoad); //relogin is not executed - don't need subscription
          if (!isEqual(filter4Call, this.filter)) return false; //means, that meanwhile a load command for another Id was executed. it must win
          const _items: T[] = [];
          if (entities) entities.map(entity => _items.push(new this.TEntity(entity)));
          if (this.focusedEntityShadowed /*&& _items.length === 0*/ /*&& this.focusedRoutingCallBack*/) // to accellerate call back for empty list, otherwise it takes up to 2s,
          // and to have isLoading=true for shadowEntity to disable new/edit button after list is loaded but before callback from the grid to load shadowEntity,
          // otherwise can happens, the edit dialog for shadowEntity is opened and shadowEntity is loaded afterwords and get editmode=view -> error
          {
            this.focusedEntityShadowed.entityWasLoaded = false; //is set to fire loaded$ event for empty too, is called when wasloaded=true in pageservice.focusedRoutingCallBack
            // functionality is called in approveFocusedSelectedItems called by this.items = _items;
            // if (_items.length === 0) {
            //   this._reloadAnyway = true; //set it to 0 to call load for id=0, that sets entityWasLoaded=false, if not focusedRoutingCallBack
            //   this.focusedItemId = 0; //set it to 0 to call dofocused immediately. don't wait on pageservice.focusedRoutingCallBack
            // }
          }
          this.items = _items; //here doFocused and do doSelected are triggered
          if (this.uiNotificationsService) this.uiNotificationsService.showMessage(this.translate.instant('core.msg.loaded', { value: this.translate.instant(this._listName), count: this.items.length }), msgID);
          if (this.afterLoadFocusFirstItem) {
            this.focusFirstItem(() => {
              if (_loadedCallback) _loadedCallback(this.items);
              if (_completeCallback) _completeCallback();
            });
          } else if (_loadedCallback) _loadedCallback(this.items);
        } catch (error) {
          afterLoadFocusFirstItem = false; //to call completecallback anyway
          const fehlerName: string = this.translate.instant('core.error.loading', { value: this.translate.instant(this._listName) });
          const errorMsg = getErrorMessageAsText(error);
          if (errorMsg !== cnst_CANCEL_ERROR) {
            if (this.uiNotificationsService) this.uiNotificationsService.showMessage(`${fehlerName}: ${errorMsg}`, msgID);
            if (!this.dontShowErrors) this.appLogsService.errorMessageBox(fehlerName, errorMsg);
            this.unsubscribe(this.loginSubscriptionLoad); //relogin is not executed - don't need subscription
          } else this.uiNotificationsService.stopMessage(msgID);
          if (_errorCallback) _errorCallback(<ResultSubject>({ errMsg: errorMsg === cnst_CANCEL_ERROR ? cnst_CANCEL_ERROR : `${fehlerName}: ${errorMsg}`, entity: this }));
        }
        finally {
          if (_completeCallback && !afterLoadFocusFirstItem) {//_completeCallback is already called as callback for this.focusFirstItem(), don't need one time more
            _completeCallback();
          }
          this.isLoading = false;
        }
      },
      (error: any) => {
        if (this.uiNotificationsService) this.uiNotificationsService.showMessage(this.translate.instant('core.error.loading', { value: this.translate.instant(this._listName) }), msgID, 6000);
        if (_errorCallback) _errorCallback(error);
        if (_completeCallback) _completeCallback();
        this.isLoading = false;
      },
    );
    return true;
  }


  updateListItem(entity: T, insertIndex: number = this.lastElement /* at the end */, wasInserted: boolean = false): number {
    if (!entity?.isEmpty() && entity.entityId !== 0) {
      if (!wasInserted) {
        let foundEntityIndex: number;
        foundEntityIndex = this.getItemIndex(entity);
        if (foundEntityIndex < 0 && this.updateItemIfWasNotFoundInList) foundEntityIndex = this.focusedItemIndex; //dont overwrite focused row in the grid, but add at the end
        if (foundEntityIndex < this.items.length && foundEntityIndex > -1) {
          if (this.items[foundEntityIndex] !== entity) {
            this.items[foundEntityIndex].assign(entity);
            // this.items[foundEntityIdx] = new this.TEntity(entity); //if is running we get spinner for the grid
          }
          if (this.focusedItemId !== entity.entityId) this.focusedItemId = entity.entityId;
          return entity.entityId;
        }
      }
      //inserts a new entity
      const newEntity: T = new this.TEntity(entity);
      if (newEntity) {
        if (insertIndex < 0 || this.items.length <= insertIndex) this.items.push(newEntity);
        else this.items.splice(insertIndex, 0, newEntity);
        this.focusedItemId = newEntity.entityId;
        return newEntity.entityId; //Id of a new Inserted entity
      }
    }
    return -1; //no entities were updated/inserted
  }


  deleteListItem(entity: T, save2deleted: boolean = true, addAsDeletedAnyway: boolean = false): boolean {
    if (!entity) return false;
    const foundEntityIdx: number = this.getItemIndex(entity);
    if (foundEntityIdx > -1) {
      if (save2deleted && (entity instanceof BaseEntity) && !entity.isNew()) this.deletedItems.push(this.items[foundEntityIdx]);
      this.items.splice(foundEntityIdx, 1); //to do delete must be called approve
      //if is deleted in the sorted grid the logic cannot be used
      if (!this.isConnectedWithSortedGrid) {
        //timeout to avoid "Check after change error"
        setTimeout(() => this.approveFocusedSelectedItems(this.items.length === 0 ? 0 : this.items[foundEntityIdx - (foundEntityIdx >= this.items.length ? 1 : 0)].entityId));
      } else if (this.items?.length === 0) this.approveFocusedSelectedItems();
      return true;
    } else if (addAsDeletedAnyway && !((entity instanceof BaseEntity) && entity.isNew())) this.deletedItems.push(entity);
    return false;
  }


  deleteAll(save2deleted: boolean = true) {
    for (let i = this.items.length - 1; i >= 0; i--) {
      const entity = this.items[i];
      if (save2deleted && !entity.isNew()) this.deletedItems.push(entity);
      // this.items.splice(i, 1); //to do delete must be called approve
    }
    this.items.splice(0);
  }


  protected doSelect(list: number[]) {
    //console.log('Selected', this.selectedItemIds);
  }


  protected doFocus(focusedId: number) {
    const focusedItemId = this.focusedEntityShadowed?.entityId ?? 0;
    const reloadAnyway = this._reloadAnyway;
    this._reloadAnyway = false;

    if (this.focusedEntityShadowed && !this.focusedRoutingCallBack) { //use direct connection or routing
      if (this.focusedEntityShadowed.entityId !== focusedId || reloadAnyway) {
        this.focusedEntityShadowed.entityId = focusedId;
      }
    }
    if (this.focusedRoutingCallBack) setTimeout(() => { this.focusedRoutingCallBack(this.focusedItem); });

    if (this.focusedCallBack && (focusedItemId !== focusedId || reloadAnyway)) {
      setTimeout(() => { this.focusedCallBack(this.focusedItem); });
    }
  }


  public async saveAsync(): Promise<boolean> {
    return new Promise<boolean>((resolve, reject) => this.save(null, () => resolve(false), () => resolve(true)));
  }


  private unsubscribe(subscr: Subscription) {
    subscr?.unsubscribe();
    subscr = null;
  }

  public async save(_loadedCallback?: () => void, _errorCallback?: ErrorCallBack, _completeCallback?: () => void) {
    let resBeforeSaveCallBack: EN_EntityBeforeSaveCallBackResults = EN_EntityBeforeSaveCallBackResults.Ok_Continue;
    // tslint:disable-next-line: no-bitwise
    const doBreak: boolean = this.beforeSaveCallBack && ((resBeforeSaveCallBack = await this.beforeSaveCallBack(this.items)) & EN_EntityBeforeSaveCallBackResults.Ok_Break) !== 0;
    if (resBeforeSaveCallBack === EN_EntityBeforeSaveCallBackResults.Ok_Break) {
      if (_loadedCallback) _loadedCallback();
      if (_completeCallback) _completeCallback();
      if (this.savedCallBack) this.savedCallBack();
      return;
    } else if (resBeforeSaveCallBack === EN_EntityBeforeSaveCallBackResults.Error_Break) {
      if (_errorCallback) _errorCallback({ errMsg: cnst_CANCEL_ERROR } as ResultSubject);
      return;
    }

    const updateDetails$: Subject<void>[] = [];
    const newItemList: BaseEntity[] = [];
    const changedItemList: BaseEntity[] = [];
    let newEntityIndex: number = 0;

    // prepare observable array for forkjoin
    this.deletedItems.forEach(() => updateDetails$.push(new Subject()));
    this.items.forEach((entity: T) => {
      if (entity.entityChanged) {
        if (entity.isNew()) {
          if (this.batchInsert) {
            const newEntity: BaseEntity = new this.TEntity(entity);
            newEntity.entityId = 0;
            newItemList.push(newEntity.deleteFields());
          } else updateDetails$.push(new Subject());
        } else changedItemList.push(entity.deleteFields());
      }
    });
    if (newItemList.length > 0) updateDetails$.push(new Subject());
    if (changedItemList.length > 0) updateDetails$.push(new Subject());

    //if nothing was deleted/added/changed
    if (updateDetails$.length === 0) {
      if (_loadedCallback) _loadedCallback();
      if (this.savedCallBack) this.savedCallBack();
      if (_completeCallback) _completeCallback();
      return;
    }

    if (!this.loginSubscriptionSave && this.hierarchyLevel === 0 && this.repositoryService.authService.user.isAdmin) {
      this.loginSubscriptionSave = this.repositoryService.authService.loginAction$.subscribe((resultObject) => { //try one time more
        if (resultObject.result) {
          this.appLogsService.dlog('!!! - relogin - resave list', resultObject);
          this.save(_loadedCallback, _errorCallback, _completeCallback);
        } else this.unsubscribe(this.loginSubscriptionSave);
      });
    } else {
      if (this.loginSubscriptionSave) this.unsubscribe(this.loginSubscriptionSave); // try only one time more
    }


    let msgID: number;
    if (this.uiNotificationsService) msgID = this.uiNotificationsService.showMessage(this.translate.instant('core.msg.saving', { value: this.translate.instant(this._listName) }));
    //wait on the result
    forkJoin(updateDetails$).subscribe(
      () => {
        this.unsubscribe(this.loginSubscriptionSave); //relogin is not executed - don't need subscription
        this.appLogsService.dlog('++ next save list <' + this.translate.instant(this._listName) + '>');
        this.items = this.items.slice();  //otherwise for new entities in the bind to grid lists id in the grid will not be updated and stay <0
        // this.approveFocusedSelectedItems();
        if (this.uiNotificationsService) this.uiNotificationsService.showMessage(this.translate.instant('core.msg.saved', { value: this.translate.instant(this._listName) }), msgID);
        if (_loadedCallback) _loadedCallback();
        if (this.savedCallBack) this.savedCallBack();
      }
      , (error: any) => {
        this.deletedItems = this.deletedItems.filter(_ => _); //let be in array only not null elements

        this.appLogsService.error('!! error save list <' + this.translate.instant(this._listName) + '>: ', { error });

        const fehlerName: string = this.translate.instant('core.error.saving', { value: this.translate.instant(this._listName) });
        const errorMsg = getErrorMessageAsText(error);
        if (errorMsg !== cnst_CANCEL_ERROR) {
          if (this.uiNotificationsService) this.uiNotificationsService.showMessage(`${fehlerName}: ${errorMsg}`, msgID);
          if (!this.dontShowErrors) this.appLogsService.errorMessageBox(fehlerName, errorMsg);
          this.unsubscribe(this.loginSubscriptionSave); //relogin is not executed - don't need subscription
        } else this.uiNotificationsService.stopMessage(msgID);
        if (_errorCallback) _errorCallback(<ResultSubject>({ errMsg: errorMsg === cnst_CANCEL_ERROR ? cnst_CANCEL_ERROR : `${fehlerName}: ${errorMsg}`, entity: this }));
      }
      , () => {
        this.appLogsService.dlog('++ complete save list <' + this.translate.instant(this._listName) + '>');
        this.deletedItems = this.deletedItems.filter(_ => _); //let be in array only not null elements
        if (_completeCallback) _completeCallback();
      }
    );

    //delete deleted entities
    for (let index = 0; index < this.deletedItems.length; index++) {
      const inx: number = index; //must be created every time for every observable call, otherwise we get the same value for all callbacks
      if (this.deletedItems[inx]) {
        this.deletedItems[inx].delete(this.repositoryService
          , () => {
            this.deletedItems[inx] = null; //element was sucessfully deleted - eliminate from the list of to be deleted
            updateDetails$[inx].next();
          }
          , (error) => updateDetails$[inx].error(error)
          , () => updateDetails$[inx].complete());
      }
    }

    newEntityIndex = this.deletedItems.length;
    let batchRouting = this.routing.toLowerCase();
    if (batchRouting.slice(-4) === '/all') batchRouting = batchRouting.slice(0, batchRouting.length - 4);
    batchRouting += '/list';

    if (this.batchInsert) {
      // insert new entities in batch
      if (newItemList.length > 0) {
        const inx: number = newEntityIndex; //must be created every time for every obserfable call, otherwise we get the same value for all callbacks
        this.repositoryService.postData<T[]>(batchRouting, newItemList)
          .subscribe(
            (entities: T[]) => {
              // let index: number = 0;
              // this.items.forEach((it: T) => {
              //   if (it.isNew() && it.entityChanged) it.assign(new this.TEntity(entities[index++]));
              // });
              this._items.splice(0, this._items.length, ...this.items.filter( //splice - technique to save array pointer and change only array content
                (it: T) => !(it.isNew() && it.entityChanged)).concat(entities.map((it: T) => new this.TEntity(it)))
              );
              this.appLogsService.dlog('--- next insert list <' + this.translate.instant(this._listName) + '>', inx, this.items);
              updateDetails$[inx].next();
            }
            , (error) => updateDetails$[inx].error(error)
            , () => {
              this.appLogsService.dlog('--- complete insert list <' + this.translate.instant(this._listName) + '>', inx);
              updateDetails$[inx].complete();
            }
          );
        newEntityIndex++;
      }
    } else {
      //insert new entities separately
      this.items.forEach((entity: T, index: number) => {
        if (entity.isNew() && entity.entityChanged) {
          const inx: number = newEntityIndex; //must be created every time for every observable call, otherwise we get the same value for all callbacks
          entity.save(this.repositoryService
            , (loadedEntity: BaseEntity) => {
              const updNext$ = updateDetails$[inx];
              const entity_ = loadedEntity; //because of collisions in callback if it is called sequentially
              if (this.afterInsertCallBack) {
                this.afterInsertCallBack(entity_, () => {
                  // loadedEntity.load(this.repositoryService, null);
                  updNext$.next();
                  updNext$.complete();
                });
              } else updNext$.next();
            }
            , (error) => updateDetails$[inx].error(error)
            , () => { if (!this.afterInsertCallBack) updateDetails$[inx].complete(); }
          );
          newEntityIndex++;
        }
      });
    }

    //save changed entities in batch
    if (changedItemList.length > 0) {
      const inx: number = newEntityIndex; //must be created every time for every obserfable call, otherwise we get the same value for all callbacks
      this.repositoryService.putData<T[]>(batchRouting, changedItemList)
        .subscribe(
          () => {
            this.items.forEach((entity: T) => { if (entity.entityChanged) entity.entityChanged = false; });
            this.appLogsService.dlog('--- next update list <' + this.translate.instant(this._listName) + '>', inx);
            updateDetails$[inx].next();
          }
          , (error: any) => updateDetails$[inx].error(error)
          , () => {
            this.appLogsService.dlog('--- complete update list <' + this.translate.instant(this._listName) + '>', inx);
            updateDetails$[inx].complete();
          }
        );
    }
  }


  getId4NewItem() {
    return this.items.reduce<number>((min: number, _item: T) => (min < _item.entityId ? min : _item.entityId), 0) - 1;
  }


  newItem(src?: Partial<T>, changed?: boolean): T {
    const item: T = new this.TEntity(src);
    if (changed !== null && changed !== undefined) item.entityChanged = changed;
    item.entityId = this.getId4NewItem();
    this.items.push(item);
    return item;
  }

  public match2KeysArray(entity: BaseEntity, fieldName4EntityKey: string, keyArray: number[], fieldName4Key: string): boolean {
    let wasChanged: boolean = false;
    for (let index = this.items.length - 1; index >= 0; index--) {
      const currItem = this.items[index];
      if (keyArray.indexOf(currItem[fieldName4Key]) === -1) {
        this.deleteListItem(currItem);
        wasChanged = true;
      }
    }
    keyArray.forEach((key) => {
      if (!this.items.find((item: T) => item[fieldName4Key] === key)) {
        const item = new this.TEntity();
        item[fieldName4EntityKey] = entity[fieldName4EntityKey];
        item[fieldName4Key] = key;
        item.entityChanged = true;
        this.items.push(item);
        wasChanged = true;
      }
    });
    return wasChanged;
  }


  public match2array(entity: BaseEntity, fieldName4EntityKey: string, fieldName4ArrayKey: string, fieldName4Key: string): boolean {
    return this.match2KeysArray(entity, fieldName4EntityKey, entity[fieldName4ArrayKey], fieldName4Key);
  }


  public onFocusedRowChanging($event) {
    if (this.focusedEntityShadowed && this.focusedEntityShadowed.entityIsLoading) $event.cancel = true;
  }

}
