import { HttpClient } from 'aurelia-fetch-client';
import { Config, TranslatedProperty, TranslatedPropertyHelper } from './../index';
import { DateComputation } from '../resources/filters/date-filter/date-computation';
import { AuthService } from '../security/auth-service';
import { Container } from 'aurelia-framework';
import { Datacontext } from './data-context';
import { CustomLogger } from "../modules/custom-logger";
import DataProperty = breeze.DataProperty;
import { IGetRowsParams } from "ag-grid-community";
import { GridDataSource } from './data-source';
import * as breeze from 'breeze-client';
import { I18N } from "aurelia-i18n";
import { ValueSelected, EnumSelected, FilterMode } from '../model/common-model';
import { CamelCase } from './../utils/came-case';
import { ValidationController } from 'aurelia-validation';
import { PredicateUtils } from './predicate-utils';
import { Box } from '../modules/box';

export interface QueryResults<T extends breeze.Entity> {
  fullCount?: number;
  results: T[];
  error: any;
}
export interface IRequestObject {
  predicates?: any;
  sortClause?: string;
  expands?: string[];
  queryParameters?: any;
  start?: number;
  end?: number;
}
export class Repository<T extends breeze.Entity> {
  validator: ValidationController;

  constructor(private manager: breeze.EntityManager, private logger: CustomLogger, private entityTypeName: string, private resourceName: string, private i18n: I18N) {
    this.datacontext = Container.instance.get(Datacontext);
    this.authService = <AuthService>Container.instance.get(AuthService);
    this.dateComputation = <DateComputation>Container.instance.get(DateComputation);
    this.config = Container.instance.get(Config);
    this.httpClient = Container.instance.get(HttpClient);
    this.validator = Container.instance.get(ValidationController);
    this.predicateUtils = Container.instance.get(PredicateUtils);
    this.box = Container.instance.get(Box);
  }
  private httpClient: HttpClient;
  private datacontext: Datacontext;
  private authService: AuthService;
  private dateComputation: DateComputation;
  private config: Config;
  private predicateUtils: PredicateUtils;
  private box: Box;

  public fetchForDataGrid(resourceNames: string, params: IGetRowsParams, expands: any = null, quickFilter: string = null, queryCriteria: GridDataSource<T>, queryParameters: any = null) {
    var orderByClause = "id";
    var results = {} as QueryResults<T>;
    var promise = new Promise<QueryResults<T>>((resolve, reject) => {
      if (params != null && params.sortModel != null) {
        var sortArray = <any[]>params.sortModel;
        orderByClause = '';
        sortArray.forEach(c => {
          if (orderByClause.length > 0) orderByClause += ',';
          orderByClause += this.handleTranslations(isArray(c.colId) ? c.colId : [c.colId]);
          if (orderByClause.indexOf('_') > -1) {
            orderByClause = orderByClause.replace('_', '.');
          }
          orderByClause += " " + c.sort;
        });
      }
      if (orderByClause.trim() == "") {
        orderByClause = null;
      }
      var predicatesFromQueryCriteria = queryCriteria != null ? queryCriteria.predicates : null;
      var predicatesFromParams = (params != null) && this.getPredicatesFromParams(params, quickFilter);
      var predicates = (predicatesFromQueryCriteria && predicatesFromParams) ? predicatesFromQueryCriteria.and(predicatesFromParams) :
        (predicatesFromQueryCriteria && !predicatesFromParams) ? predicatesFromQueryCriteria :
          (!predicatesFromQueryCriteria && predicatesFromParams) ? predicatesFromParams : null;

      var entityQuery = breeze.EntityQuery;
      var query: breeze.EntityQuery;
      if (params != null && params.startRow != null) {
        query = entityQuery.from(resourceNames)
          .take(params.endRow - params.startRow)
          .skip(params.startRow)
          .orderBy(orderByClause)
          .inlineCount();
      } else {
        query = entityQuery.from(resourceNames)
          .orderBy(orderByClause)
          .inlineCount();
      }

      if (predicates != null)
        query = query.where(predicates);
      if (expands != null)
        query = query.expand(expands);
      if (queryParameters != null)
        query = query.withParameters(queryParameters);
      if (queryCriteria.selectPropertyPaths?.length > 0) {
        query = query.select(queryCriteria.selectPropertyPaths);
      }
      query = query.using(breeze.FetchStrategy.FromServer);

      this.manager.executeQuery(query)
        .then((data) => {
          results.fullCount = data.inlineCount;
          results.results = <T[]>data.results;
          resolve(results);
        })
        .catch((error) => {
          this.queryFailed(error);
          results.error = error;
          reject(results);
        });
    });

    return promise;
  }
  public getEntityById(objectId: any, expands: string[], fromLocalCache: boolean = false) {
    var entityQuery = breeze.EntityQuery;
    var query = entityQuery.from(this.resourceName).where("id", breeze.FilterQueryOp.Equals, objectId);
    if (expands != null && expands.length > 0)
      query = query.expand(expands);
    query = query.using(fromLocalCache ? breeze.FetchStrategy.FromLocalCache : breeze.FetchStrategy.FromServer);

    return this.manager.executeQuery(query)
      .then((data) => {
        return <T>data.results[0];
      })
      .catch((error) => {
        this.queryFailed(error);
        return <T>null;
      });
  }
  public getAllEntities(expands: string[]) {
    var entityQuery = breeze.EntityQuery;
    var query = entityQuery.from(this.resourceName);
    if (expands != null && expands.length > 0)
      query = query.expand(expands);

    query = query.using(breeze.FetchStrategy.FromServer);

    return this.manager.executeQuery(query)
      .then((data) => {
        return <Array<T>>data.results;
      })
      .catch((error) => {
        this.queryFailed(error);
        return <Array<T>>null;
      });
  }
  public getEntities(predicates: breeze.Predicate = null, expands: string[], parameters: any, orderBy?: { propertyPaths: string, isDescending?: boolean }, paging?: { skip?: number, take?: number, pageNumber?: number }) {
    return this.internalGetEntities(breeze.FetchStrategy.FromServer, predicates, expands, parameters, orderBy, paging);
  }

  public getEntitiesFromLocalCache(predicates: breeze.Predicate = null, expands: string[], orderBy?: { propertyPaths: string, isDescending?: boolean }, paging?: { skip?: number, take?: number, pageNumber?: number }) {
    return this.internalGetEntities(breeze.FetchStrategy.FromLocalCache, predicates, expands, null, orderBy, paging);
  }
  public getCount(predicates: breeze.Predicate = null, parameters: any): Promise<number> {
    var entityQuery = breeze.EntityQuery;
    var query = entityQuery.from(this.resourceName);
    if (predicates != null) {
      query = query.where(predicates);
    }
    if (parameters != null) {
      query = query.withParameters(parameters);
    }
    query = query.take(0).inlineCount(true);
    query = query.using(breeze.FetchStrategy.FromServer);

    return this.manager.executeQuery(query)
      .then((data) => {
        return data.inlineCount;
      })
      .catch((error) => {
        this.queryFailed(error);
        return 0;
      });
  }
  public firstEntity(predicates: breeze.Predicate = null, expands: string[], parameters: any, orderBy?: { propertyPaths: string, isDescending?: boolean }, skip?: number): Promise<T> {
    var entityQuery = breeze.EntityQuery;
    var query = entityQuery.from(this.resourceName);
    if (expands != null && expands.length > 0) {
      query = query.expand(expands);
    }
    if (predicates != null) {
      query = query.where(predicates);
    }
    if (parameters != null) {
      query = query.withParameters(parameters);
    }
    if (orderBy != null) {
      query = query.orderBy(orderBy.propertyPaths, orderBy.isDescending ?? false);
    }
    if (skip != null) {
      query = query.skip(skip);
    }
    query = query.take(1).using(breeze.FetchStrategy.FromServer);

    return this.manager.executeQuery(query)
      .then((data) => {
        return <T>data.results[0];
      })
      .catch((error) => {
        this.queryFailed(error);
        return <T>null;
      });
  }
  public getEntitiesFromCache(entityState: breeze.EntityStateSymbol): breeze.Entity[] {
    return this.manager.getEntities(this.entityTypeName, entityState);
  }
  public addEntityInCacheIfNotPresent(id: number, observableEntity: T, initialValues?: any, entityState?: breeze.EntityStateSymbol) {
    var entity = <T>this.manager.getEntityByKey(this.entityTypeName, id);
    var promise = new Promise<T>((resolve) => {
      if (entity == null) {
        this.createEntity(initialValues, entityState).then((entity) => {
          resolve(entity);
        });
      }
      else {
        resolve(entity);
      }
    });

    return promise;
  }
  public findAddedEntity(initialValues) {
    var addedEntities = this.getEntitiesFromCache(breeze.EntityState.Added);
    var filterPredicate = (entity) => {
      var ret = true;
      for (var p in initialValues) {
        if (initialValues.hasOwnProperty(p)) {
          ret = ret && initialValues[p] == entity[p];
        }
      }
      return ret;
    }
    var existingAddedEntity = addedEntities.filter(filterPredicate);
    if (existingAddedEntity != null && existingAddedEntity.length === 1) {
      return existingAddedEntity[0];
    }
    return null;
  }
  public restoreDeletedEntity(initialValues: any): any {
    var deletedEntities = this.getEntitiesFromCache(breeze.EntityState.Deleted);
    var filterPredicate = (entity) => {
      var ret = true;
      for (var p in initialValues) {
        if (initialValues.hasOwnProperty(p)) {
          ret = ret && initialValues[p] == entity[p];
        }
      }
      return ret;
    }
    var existingDeletedEntity = deletedEntities.filter(filterPredicate);

    if (existingDeletedEntity != null && existingDeletedEntity.length == 1) {
      existingDeletedEntity[0].entityAspect.rejectChanges();
      return existingDeletedEntity[0];
    }
    return null;
  }
  public createEntity(initialValues?: any, entityState?: breeze.EntityStateSymbol): Promise<T> {
    return new Promise<T>(resolve => resolve(this.createEntityHelper(initialValues, entityState)));
  }
  private resolveReferences(json: any) {
    if (typeof json === 'string')
      json = JSON.parse(json);

    var byid = {}, // all objects by id
      refs = []; // references to objects that could not be resolved
    json = (function recurse(obj, prop, parent) {
      if (typeof obj !== 'object' || !obj) // a primitive value
        return obj;
      if (Object.prototype.toString.call(obj) === '[object Array]') {
        for (var i = 0; i < obj.length; i++)
          // check also if the array element is not a primitive value
          if (typeof obj[i] !== 'object' || !obj[i]) // a primitive value
            continue;
          else if ("$ref" in obj[i])
            obj[i] = recurse(obj[i], i, obj);
          else
            obj[i] = recurse(obj[i], prop, obj);
        return obj;
      }
      if ("$ref" in obj) { // a reference
        var ref = obj.$ref;
        if (ref in byid)
          return byid[ref];
        // else we have to make it lazy:
        refs.push([parent, prop, ref]);
        return;
      } else if ("$id" in obj) {
        var id = obj.$id;
        delete obj.$id;
        if ("$values" in obj) // an array
          obj = obj.$values.map(recurse);
        else // a plain object
          for (var propObj in obj)
            obj[propObj] = recurse(obj[propObj], propObj, obj);
        byid[id] = obj;
      }
      return obj;
    })(json); // run it!

    for (var i = 0; i < refs.length; i++) { // resolve previously unknown references
      var ref = refs[i];
      ref[0][ref[1]] = byid[ref[2]];
      // Notice that this throws if you put in a reference at top-level
    }
    return json;
  }
  public async createEntityFromFactory<TParameters>(factoryRequest: Request, overriddenInitialValues?: any): Promise<{ entity: T, parameters: TParameters }> {
    const factoryResponse = await this.httpClient.post(factoryRequest);
    if (factoryResponse.ok) {
      let factoryResult = JSON.parse(await factoryResponse.text(), (key: string, value: any) => { return CamelCase.convertKeysToCamelCase(key, value) });
      let defaultEntityValues = this.resolveReferences(factoryResult.entity);
      if (overriddenInitialValues != null) {
        defaultEntityValues = Object.assign(defaultEntityValues, overriddenInitialValues);
      }
      let createdEntity = await this.createEntityHelper(defaultEntityValues);
      this.setFKToUnChanged(createdEntity, 0);
      return { entity: createdEntity, parameters: factoryResult.parameters as TParameters };
    }
    else {
      throw factoryResponse.text();
    }
  }
  private setFKToUnChanged(entity: breeze.Entity, level: number) {
    if (level > 5) return;
    entity.entityType.navigationProperties.forEach(navigationProperty => {
      if (navigationProperty.isScalar) {
        const currentFkObject = entity[navigationProperty.name] as breeze.Entity & { id: number };
        if (currentFkObject != null && currentFkObject.entityAspect.entityState === breeze.EntityState.Added) {
          currentFkObject.entityAspect.setUnchanged();
        }
        if (currentFkObject != null) {
          this.setFKToUnChanged(currentFkObject, level + 1);
        }
      }
    });
  }
  private setFKDataPropertyToNullWhenNumericNonNullable(entity: breeze.Entity) {
    entity.entityType.navigationProperties.forEach(navigationProperty => {
      if (navigationProperty.isScalar) {
        const currentFkObject = entity[navigationProperty.name] as breeze.Entity & { id: number };
        if (currentFkObject == null) {
          navigationProperty.foreignKeyNames.forEach(foreignKeyName => {
            let dataProperty = entity.entityType.dataProperties.find(x => x.name == foreignKeyName);
            if (dataProperty != null && !dataProperty.isComplexProperty && dataProperty.isSettable && !dataProperty.isNullable && dataProperty.dataType.isNumeric && entity[foreignKeyName] != null && entity[foreignKeyName] <= 0) {
              entity[foreignKeyName] = null;
            }
          });
        }
      }
    });
  }
  public createEntitySync(initialValues?: any, entityState?: breeze.EntityStateSymbol): T {
    return this.createEntityHelper(initialValues, entityState);
  }
  private createEntityHelper(initialValues?: any, entityState?: breeze.EntityStateSymbol): T {
    if (initialValues == null)
      initialValues = {};

    var entityType = <breeze.EntityType>this.manager.metadataStore.getEntityType(this.entityTypeName);

    if (entityType.autoGeneratedKeyType != breeze.AutoGeneratedKeyType.Identity && entityType.keyProperties.findIndex((val) => val.name === "id") >= 0)
      if (initialValues.id == undefined)
        initialValues.id = -((new Date()).getTime() & 0xffff); //Constants.Various.NewId;

    let createdEntity: T = <T>this.manager.createEntity(this.entityTypeName, initialValues, entityState, breeze.MergeStrategy.SkipMerge);
    this.setFKDataPropertyToNullWhenNumericNonNullable(createdEntity);
    return createdEntity;
  }
  public detachEntity(entity: breeze.Entity): void {
    entity.entityAspect.setDetached();
  }
  public lockRecordForEditing(entity: breeze.Entity) {
    var saveOptions = new breeze.SaveOptions();
    saveOptions.resourceName = "lockRecordForEditing";
    return this.manager.saveChanges([entity], saveOptions)
      .then((saveSucceeded) => {
        this.logger.Log("Lock the record for editing", saveSucceeded, 'Repository', false);
      })
      .catch((errors) => {
        this.logger.LogError("Errors when locking the record", errors, 'Repository', false);
        if (errors.entityErrors) {
          this.queryFailed(errors);
        }
        return Promise.reject(errors);
      });
  }
  public saveChanges(entities?: breeze.Entity[], silentSave?: boolean, byPassLocking?: boolean) {

    var options = new breeze.SaveOptions();
    if (byPassLocking === true) {
      options.tag = "BY_PASS_LOCKING";
    }
    return this.manager.saveChanges(entities?.filter(entity => entity.entityAspect.entityState.isDetached() == false), options)
      .then((saveSucceeded) => {
        this.logger.Log(this.i18n.tr('general.successfullySaved', { ns: "common" }), saveSucceeded, 'Repository', !silentSave);
      })
      .catch((errors) => {
        this.queryFailed(errors);
        return Promise.reject(errors);
      });
  }

  public cancelChanges(silentMode: boolean = false) {
    this.manager.rejectChanges();
    this.logger.Log(this.i18n.tr('general.cancelModifications', { ns: "common" }), null, 'Repository', !silentMode);
  }

  public deleteEntities(entities: T[], detachInverseFKEntities: boolean = false, beforeSaveAction: ((entity: T) => void) = null) {
    let entitiesTemp = [...entities];
    let entitiesLength: number = entities.length;
    while (entitiesLength > 0) {
      let entity: T = entities[entitiesLength - 1];
      if (entity.entityAspect.entityState.isAdded()) {
        entity.entityAspect.setDetached();
      }
      else {
        entity.entityAspect.setDeleted();
      }
      if (beforeSaveAction != null) {
        beforeSaveAction(entity);
      }
      if (detachInverseFKEntities) {
        let inverseForeignKeyProperties = <DataProperty[]>(<any>entity.entityType).inverseForeignKeyProperties;
        inverseForeignKeyProperties.forEach((inverseFK) => {
          if (inverseFK.parentType.name != entity.entityType.name) {
            let changes: breeze.Entity[] = this.manager.getChanges(inverseFK.parentType.name);
            let changesLength: number = changes.length;
            while (changesLength > 0) {
              let change = changes[changesLength - 1];
              change.entityAspect.setDetached()
              changesLength--;
            }
          }
        });
      }
      entitiesLength--;
    }
    return this.saveChanges(entitiesTemp, true);
  }

  protected internalGetEntities(fetchStrategy: breeze.FetchStrategySymbol, predicates: breeze.Predicate = null, expands: string[], parameters: any, orderBy?: { propertyPaths: string, isDescending?: boolean }, paging?: { skip?: number, take?: number, pageNumber?: number }) {
    var entityQuery = breeze.EntityQuery;
    var query = entityQuery.from(this.resourceName);
    if (expands != null && expands.length > 0)
      query = query.expand(expands);

    if (predicates != null) {
      query = query.where(predicates);
    }
    if (parameters != null) {
      query = query.withParameters(parameters);
    }
    if (orderBy != null) {
      query = query.orderBy(orderBy.propertyPaths, orderBy.isDescending ?? false);
    }
    if (paging != null) {
      if (paging.pageNumber != null && paging.take != null) {
        query = query.skip((paging.pageNumber - 1) * paging.take);
        query = query.take(paging.take);
      } else {
        if (paging.take) {
          query = query.take(paging.take);
        }
        if (paging.skip) {
          query = query.skip(paging.skip);
        }
      }
    }

    query = query.using(fetchStrategy);

    return this.manager.executeQuery(query)
      .then((data) => {
        return <Array<T>>data.results;
      })
      .catch((error) => {
        this.queryFailed(error);
        return <Array<T>>null;
      });
  }

  private getPredicatesFromParams(params: IGetRowsParams, quickFilter: string) {
    let nbOfOrPredicate: number = 0;
    let predicates: Array<breeze.Predicate> = new Array<breeze.Predicate>();
    let filterModel = params.filterModel;
    if (filterModel && Object.keys(filterModel).length > 0) {
      Object.keys(filterModel).forEach(f => {
        let filter = filterModel[f];

        if (filter != null) {
          // Filter type
          let operator: breeze.FilterQueryOpSymbol;
          if (filter.type != null) {
            operator = this.getBreezeOperatorFromFilterType(filter.type);
          }

          if (filter.filterType == "text") {
            let tempPredicate: breeze.Predicate;
            if (filter.filter != null) {
              let textPredicates: Array<breeze.Predicate> = this.predicateUtils.getPredicates(filter.filter, this.handleTranslation(f), breeze.DataType.String, operator);
              if (textPredicates.length > 0) {
                tempPredicate = breeze.Predicate.and(...textPredicates);
              }
            }
            if (filter.displayEmptyFields) {
              tempPredicate = this.addNullabilityToPredicate(tempPredicate, this.handleTranslation(f), true)
                .or(new breeze.Predicate(this.handleTranslation(f), breeze.FilterQueryOp.Equals, ""));
            }
            if (tempPredicate != null) {
              predicates.push(tempPredicate);
            }
          }
          else if (filter.filterType == "number") {
            let tempPredicate: breeze.Predicate;
            if (filter.filter || filter.filterTo) {
              if (filter.type === "inRange") {
                tempPredicate = new breeze.Predicate(f, breeze.FilterQueryOp.LessThanOrEqual, filter.filterTo).and(new breeze.Predicate(f, breeze.FilterQueryOp.GreaterThanOrEqual, filter.filter));
              }
              else {
                tempPredicate = new breeze.Predicate(f, operator, (filter.filter ? filter.filter : filter.filterTo));
              }
            }
            if (filter.displayEmptyFields) {
              tempPredicate = this.addNullabilityToPredicate(tempPredicate, f, true);
            }
            predicates.push(tempPredicate);
          }
          else if (filter.filterType == "boolean") {
            predicates.push(new breeze.Predicate(f, breeze.FilterQueryOp.Equals, filter.filter));
          }
          else if (filter.filterType == "date") {
            let dateInterval = this.dateComputation.GetDateIntervalFromFilter(filter);

            let tempPredicate: breeze.Predicate;
            if (dateInterval.dateFrom != null) {
              tempPredicate = new breeze.Predicate(f, breeze.FilterQueryOp.GreaterThanOrEqual, dateInterval.dateFrom.toDate());
            }
            if (dateInterval.dateTo != null) {
              tempPredicate = breeze.Predicate.and(tempPredicate, new breeze.Predicate(f, breeze.FilterQueryOp.LessThanOrEqual, dateInterval.dateTo.toDate()));
            }
            if (filter.isDateNullable) {
              nbOfOrPredicate++;
              tempPredicate = this.addNullabilityToPredicate(tempPredicate, f, true);
            }
            predicates.push(tempPredicate);
          }
          else if (filter.filterType == "set") {
            let tempPredicates: breeze.Predicate = null;
            filter.values.forEach(value => {
              if (tempPredicates == null) tempPredicates = new breeze.Predicate(f, breeze.FilterQueryOp.Equals, value);
              else tempPredicates = tempPredicates.or(new breeze.Predicate(f, breeze.FilterQueryOp.Equals, value));
            });
            if (tempPredicates) {
              nbOfOrPredicate++;
              predicates.push(tempPredicates);
            }
          }
          else if (filter.filterType == "enumeration") {
            let enumerationValues: Array<ValueSelected> = filter.filter;
            let tempPredicates: breeze.Predicate;
            let property: string = filter.relationIdFieldName?.length > 0 ? filter.relationIdFieldName : this.handleTranslation(f);
            enumerationValues.forEach(enumerationValue => {
              if (enumerationValue.selected) {
                let predicate: breeze.Predicate;
                if (filter.relationIdFieldName?.length > 0) {
                  predicate = new breeze.Predicate(property, breeze.FilterQueryOp.Equals, enumerationValue.id);
                } else {
                  predicate = new breeze.Predicate(property, breeze.FilterQueryOp.Equals, enumerationValue.name);
                }
                tempPredicates = tempPredicates != null ? tempPredicates.or(predicate) : predicate;
              }
            });
            if (filter.displayEmptyFields) {
              nbOfOrPredicate++;
              tempPredicates = this.addNullabilityToPredicate(tempPredicates, property, true);
            }
            if (tempPredicates) {
              nbOfOrPredicate++;
              predicates.push(tempPredicates);
            }
          }
          else if (filter.filterType == "enum") {
            let enumValues: Array<EnumSelected> = filter.filter;
            let tempPredicates: breeze.Predicate;
            enumValues.forEach(enumValue => {
              if (enumValue.selected) {
                if (tempPredicates == null) tempPredicates = new breeze.Predicate(f, breeze.FilterQueryOp.Equals, enumValue.index);
                else tempPredicates = tempPredicates.or(new breeze.Predicate(f, breeze.FilterQueryOp.Equals, enumValue.index));
              }
            });
            if (tempPredicates) {
              nbOfOrPredicate++;
              predicates.push(tempPredicates);
            }
          }
          else if (filter.filterType == "user") {
            let fieldKey = f;
            let byName = true;
            if (/ByFullName$/.test(fieldKey)) {
              fieldKey = f.replace(/ByFullName$/, 'ById');
              byName = false;
            }
            if (operator != null) {
              if (filter.userPreFilter != null) {
                filter.filter = this.authService.currentUser[byName ? 'fullName' : 'id'];
              }
              predicates.push(new breeze.Predicate(fieldKey, operator, filter.filter));
            } else {
              let userValues: Array<ValueSelected> = filter.filter;
              let tempPredicates: breeze.Predicate;
              userValues.forEach(userValue => {
                if (userValue.selected) {
                  if (tempPredicates == null) tempPredicates = new breeze.Predicate(fieldKey, breeze.FilterQueryOp.Equals, userValue[byName ? 'name' : 'id']);
                  else tempPredicates = tempPredicates.or(new breeze.Predicate(fieldKey, breeze.FilterQueryOp.Equals, userValue[byName ? 'name' : 'id']));
                }
              });
              if (filter.displayEmptyFields) {
                nbOfOrPredicate++;
                tempPredicates = this.addNullabilityToPredicate(tempPredicates, fieldKey, true);
              }
              if (tempPredicates) {
                nbOfOrPredicate++;
                predicates.push(tempPredicates);
              }
            }
          }
          else if (filter.filterType == "manyToMany" && f.includes('.')) {
            let values: Array<ValueSelected> = filter.filter;

            let allFieldPath: Array<string> = f.split('.');
            let arrayIndex = (filter as any).arrayIndex ?? 1;
            let arrayName = allFieldPath.slice(0, arrayIndex + 1).join(".");

            let fieldPath: string = allFieldPath.slice(arrayIndex + 1).join(".");
            let tempPredicates: breeze.Predicate;
            values.forEach(value => {
              if (value.selected) {
                if (tempPredicates == null) tempPredicates = new breeze.Predicate(arrayName, breeze.FilterQueryOp.Any, this.handleTranslation(fieldPath), breeze.FilterQueryOp.Equals, value.name);
                else {
                  // Filter mode
                  if (filter.filterMode == FilterMode.And) {
                    tempPredicates = tempPredicates.and(new breeze.Predicate(arrayName, breeze.FilterQueryOp.Any, this.handleTranslation(fieldPath), breeze.FilterQueryOp.Equals, value.name));
                  }
                  else {
                    tempPredicates = tempPredicates.or(new breeze.Predicate(arrayName, breeze.FilterQueryOp.Any, this.handleTranslation(fieldPath), breeze.FilterQueryOp.Equals, value.name));
                  }
                }
              }
            });
            if (filter.displayEmptyFields) {
              nbOfOrPredicate++;
              tempPredicates = this.addNullabilityToPredicate(tempPredicates, arrayName, true, fieldPath.includes("_translation") ? this.handleTranslation(fieldPath) : fieldPath);
            }
            if (tempPredicates) {
              nbOfOrPredicate++;
              predicates.push(tempPredicates);
            }
          }
          else if (filter.filterType == "oneToMany") {
            let values: Array<ValueSelected> = filter.filter;
            let tempPredicates: breeze.Predicate;
            let property: string = filter.relationIdFieldName?.length > 0 ? filter.relationIdFieldName : this.handleTranslation(f);
            values.forEach(value => {
              if (value.selected) {
                let predicate: breeze.Predicate;
                if (filter.relationIdFieldName?.length > 0) {
                  predicate = new breeze.Predicate(property, breeze.FilterQueryOp.Equals, value.id);
                } else {
                  predicate = new breeze.Predicate(property, breeze.FilterQueryOp.Equals, value.name);
                }
                tempPredicates = tempPredicates != null ? tempPredicates.or(predicate) : predicate;
              }
            });
            if (filter.displayEmptyFields) {
              nbOfOrPredicate++;
              tempPredicates = this.addNullabilityToPredicate(tempPredicates, property, true);
            }
            if (tempPredicates) {
              nbOfOrPredicate++;
              predicates.push(tempPredicates);
            }
          }
          else if (filter.type == null && filter.length > 0) {
            let filterArray = filter as Array<string>;
            let tempPredicates: breeze.Predicate = null;
            filterArray.forEach((value) => {
              if (tempPredicates == null) {
                tempPredicates = new breeze.Predicate(this.handleTranslation(f), breeze.FilterQueryOp.Equals, value);
              }
              else {
                tempPredicates = tempPredicates.or(new breeze.Predicate(this.handleTranslation(f), breeze.FilterQueryOp.Equals, value));
              }
            });
            if (tempPredicates) {
              nbOfOrPredicate++;
              predicates.push(tempPredicates);
            }
          }
        }
      });
    }
    let p: breeze.Predicate;
    if (predicates.length > 0) {
      if (predicates.length > 1 && nbOfOrPredicate > 1) {
        //Find condition for prioritizing the primary key first
        let dataProperty: DataProperty = (this.manager.metadataStore.getEntityType(this.entityTypeName) as breeze.EntityType).keyProperties.find((val) => !val.isNullable && (val.isPartOfKey || !val.isPartOfKey));
        if (dataProperty != null) {
          predicates.push(new breeze.Predicate(dataProperty.name, breeze.FilterQueryOp.NotEquals, null));
        }
      }
      p = breeze.Predicate.and(predicates);
    }
    p = this.fillPredicatesWithQuickFilter(p, quickFilter);
    return p;
  }

  private handleTranslation(field: string): string {
    return this.handleTranslations([field])[0];
  }

  private handleTranslations(fields: string[]): string[] {
    return fields.map(field => field.replace('_translation', this.datacontext.translatePropertyName));
  }

  private getBreezeOperatorFromFilterType(filterType: string): breeze.FilterQueryOpSymbol {
    switch (filterType) {
      case 'contains':
        return breeze.FilterQueryOp.Contains;
      case 'notEqual':
        return breeze.FilterQueryOp.NotEquals;
      case 'inRange':
        return breeze.FilterQueryOp.Equals;
      case 'equals':
        return breeze.FilterQueryOp.Equals;
      case 'startsWith':
        return breeze.FilterQueryOp.StartsWith;
      case 'endsWith':
        return breeze.FilterQueryOp.EndsWith;
      case 'greaterThan':
        return breeze.FilterQueryOp.GreaterThanOrEqual;
      case 'lessThan':
        return breeze.FilterQueryOp.LessThanOrEqual;
      default:
        return null;
    }
  }

  private addNullabilityToPredicate(predicate: breeze.Predicate, property: string, isNullable: boolean, fieldPath: string = null) {
    let nullablePredicate: breeze.Predicate;
    if (isNullable) {
      // property is table
      // 1st condition : table contains a fieldpath != null + take inverse
      nullablePredicate = fieldPath != null ?
        new breeze.Predicate(property, breeze.FilterQueryOp.Any, fieldPath, breeze.FilterQueryOp.NotEquals, null).not()
        : new breeze.Predicate(property, breeze.FilterQueryOp.Equals, null);
      return predicate ? predicate.or(nullablePredicate) : nullablePredicate;
    }
    return predicate;
  }

  private fillPredicatesWithQuickFilter(predicate: breeze.Predicate, quickFilter: string) {
    if (quickFilter != null && quickFilter.length > 0) {
      let entityTypeName = this.manager.metadataStore.getEntityTypeNameForResourceName(this.resourceName);
      let entityType = this.manager.metadataStore.getEntityType(entityTypeName);
      let languages = Container.instance.get(Config).globalConfig.configuredLanguages || [];
      let columnIndex = languages.find(x => x.code == this.i18n.getLocale())?.columnIndex;
      let currentPredicate: breeze.Predicate;
      entityType.dataProperties
        .filter(dp =>
          (dp.dataType == breeze.DataType.String || (<any>dp.dataType).name == TranslatedPropertyDataType)
          && (<any>dp).enumType == null
          && dp.isUnmapped != true
        )
        .forEach(dp => {
          let predicateQuickFilter = this.quickFilterGetPredicate(dp, quickFilter, columnIndex);
          if (predicateQuickFilter.length > 0) {
            let newPredicates: breeze.Predicate = breeze.Predicate.and(predicateQuickFilter);
            if (currentPredicate == null) {
              currentPredicate = newPredicates;
            }
            else {
              currentPredicate = currentPredicate.or(newPredicates);
            }
          }
        });
      if (predicate != null && currentPredicate != null) {
        return predicate.and(currentPredicate);
      }
      return predicate ?? currentPredicate;
    }
    return predicate;
  }

  private quickFilterGetPredicate(dp: DataProperty, quickFilter: string, columnIndex: number) {
    if (dp.dataType == breeze.DataType.String) {
      return this.predicateUtils.getPredicates(quickFilter, dp.name, breeze.DataType.String, breeze.FilterQueryOp.Contains);
    } else if ((<any>dp.dataType).name == TranslatedPropertyDataType) {
      if (columnIndex != null) {
        return this.predicateUtils.getPredicates(quickFilter, dp.name + '.lang' + columnIndex, breeze.DataType.String, breeze.FilterQueryOp.Contains);
      }
    }
  }

  private isKnownServerSideError(entityError: any): { isKnown: boolean, translation?: string } {
    const translationKey: string = 'serverExceptions.' + entityError.errorName;
    if (this.i18n.i18next.exists(translationKey, { ns: "common" })) {
      return {
        isKnown: true,
        translation: this.i18n.tr(translationKey, { ns: "common" })
      }
    }
    if (this.i18n.i18next.exists(translationKey)) {
      return {
        isKnown: true,
        translation: this.i18n.tr(translationKey)
      }
    }
    return {
      isKnown: false
    };
  }

  private queryFailed(errors: any) {
    if (errors.entityErrors) {
      (<Array<any>>errors.entityErrors).forEach((entityError) => {
        let responseIsKnown = this.isKnownServerSideError(entityError);
        if (responseIsKnown.isKnown) {
          let message: string = responseIsKnown.translation;
          if (entityError.errorName == "ELEMENT_COULD_NOT_BE_DELETED") {
            message += this.getElementCouldNotBeDeletedMessage(entityError);
          }
          this.box.showError(message, this.i18n.tr('general.error', { ns: "common" }), null, false);
        }
        else if (entityError.entity) {
          if (entityError.errorName == "VALIDATION_EXCEPTION") {
            let message: string = this.getValidationException(entityError);
            this.validator.addError(message, entityError.entity, entityError.propertyName);
            if (message != null) {
              this.logger.LogError(message, errors, 'Repository', true);
            }
            else {
              this.getDefaultMessage(entityError, errors);
            }
          }
          else {
            this.logger.LogError(entityError.errorMessage, errors, 'Repository', (entityError.propertyName && entityError.propertyName.length > 0));
          }
        }
        else {
          this.getDefaultMessage(entityError, errors, true);
        }
      });
    }
    else {
      if (errors.status == null) {
        // server not available
        this.logger.LogError(this.i18n.tr('serverExceptions.serverNotAvailable', { ns: 'common' }), errors, 'Repository', true);
      }
      else if (errors.status == 414) {
        this.logger.LogError(this.i18n.tr('serverExceptions.uriTooLong', { ns: 'common' }), errors, 'Repository', true);
      }
      else if (errors.status == 500) {
        let message: string;
        if (errors.message != null && typeof errors.message == "string" && errors.message.length > 0) {
          message = errors.message;
        }
        else {
          message = this.i18n.tr("serverExceptions.unattendedError", { ns: "common" });
        }
        this.logger.LogError(message, errors, 'Repository', true);
      }
      else {
        this.logger.LogError(errors, errors, 'Repository', true);
      }
    }
  }

  private getElementCouldNotBeDeletedMessage(element: { errorMessage: string, errorName: string }): string {
    let message: string = '<ul class="browser-default">';
    try {
      let entitiesToDel: Array<{ count: number, entityName: string, entityPropertyName: string, referencingName: string, referencingPropertyName: string, displayValues: Array<string | TranslatedProperty> }> = JSON.parse(element.errorMessage, (key: string, value: any) => { return CamelCase.convertKeysToCamelCase(key, value) });
      message += entitiesToDel.map(entityToDel => {
        let entityName: string = entityToDel.entityName.toLowerCase();
        let message = `<li>${entityToDel.count} ${this.i18n.tr(entityName + '.' + entityName)}`;
        if (entityToDel.displayValues != null && entityToDel.displayValues.length > 0) {
          let entityPropertyName: string = CamelCase.toCamelCase(entityToDel.entityPropertyName);

          let displayValue: string;
          if (typeof entityToDel.displayValues[0] == "string" || entityToDel.displayValues[0] instanceof String) {
            displayValue = entityToDel.displayValues.join(", ");
          }
          else {
            displayValue = entityToDel.displayValues.map((x: TranslatedProperty) => x[this.datacontext.translatePropertyName]).join(", ");
          }

          let informationMessage: string;
          if (entityToDel.displayValues.length > 1) {
            informationMessage = 'serverExceptions.ELEMENT_COULD_NOT_BE_DELETED_INFORMATIONS';
          }
          else {
            informationMessage = 'serverExceptions.ELEMENT_COULD_NOT_BE_DELETED_INFORMATION';
          }

          message += ` ${this.i18n.tr(informationMessage, { ns: "common", displayName: this.i18n.tr(entityName + '.' + entityPropertyName) })} ${displayValue}`;
        }
        return message + '</li>';
      }).join('');
    }
    catch (error) { }
    finally {
      return message + '</ul>';
    }
  }

  private getDefaultMessage(entityError, errors, showToast = true) {
    let keyTr: string = 'serverExceptions.' + entityError.errorName;
    let message: string = this.i18n.tr(keyTr, { ns: "common" });
    if (message == keyTr) {
      message = this.i18n.tr(keyTr);
      if (message == keyTr) {
        message = entityError.errorMessage;
      }
    }
    this.logger.LogError(message, errors, 'Repository', showToast);
  }

  private getValidationException(entityError: { errorMessage: string, errorName: string, propertyName: string }): string {
    if (entityError.errorName == "VALIDATION_EXCEPTION" && entityError.propertyName != null) {
      let errorParameter: ErrorValidation = JSON.parse(entityError.errorMessage, (key: string, value: any) => { return CamelCase.convertKeysToCamelCase(key, value) });
      if (errorParameter != null) {
        if (errorParameter.errorMessageTr != null) {
          return this.i18n.tr(errorParameter.errorMessageTr, errorParameter.parameters);
        } else {
          if (errorParameter.errorMessage != null) {
            return errorParameter.errorMessage
          }
        }
      }

    }
    return null;
  }
}

const TranslatedPropertyDataType = "TranslatedProperty:#DWFramework.Web.API.Model";
interface ErrorValidation {
  attribute: string;
  entityName: string;
  errorMessage: string;
  errorMessageTr: string;
  parameters: any;
}