import { IFilterParams } from 'ag-grid-community';
import { Disposable, observable } from "aurelia-framework";
import * as breeze from 'breeze-client';
import BaseFilter from "./base-filter";
import { ValueSelected } from "../../model/common-model";
import { ServiceBase } from "../../data/service-base";
import { toRegex } from "diacritic-regex";

const MAX_SET_MODEL_RETRIES = 50;
export abstract class BaseRelationFilter<T extends breeze.Entity> extends BaseFilter {
  public filterValues: Array<ValueSelected>;
  public filterValuesSelected: Array<ValueSelected> = [];
  public selectedIds: Array<number> = [];
  protected service: ServiceBase<T>;
  protected fieldPath: Array<string>;
  protected relationIdFieldName: string;
  protected abstract filterType: string;
  protected displayEmptyFields: boolean = false;

  private selectedIdInternalChange: boolean = false;
  private selectedIdsObserver: Disposable;
  private originalValues: Array<T>;
  private currentSetModelRetries: number = 0;

  @observable
  private valueSearch: string;
  ignoreChanges: boolean = false;;

  public init(params: IFilterParams) {
    this.filterChangedCallback = params.filterChangedCallback;
    super.init(params);
    if ((params as any).customdFieldPath != null) {
      this.fieldPath = (params as any).customdFieldPath;
    } else {
      this.fieldPath = params.column.getColDef().field.split('.');
      this.fieldPath.splice(0, 1);
    }
    this.firstTime = false;
    this.loadValues();
  }

  public valueSearchChanged(newVal: string, oldVal: string) {
    if (newVal != oldVal) {
      const regExp = new RegExp(toRegex({ flags: 'imu' })(newVal.trim().replace(/\/|\\/g, '')), 'imu');// Use toRegex() to manage diacritics
      this.filterValues.forEach(filterValue => {
        let result = regExp.exec(filterValue.name.trim());
        filterValue.hide = !(result?.length > 0);
      });
    }
  }

  public isFilterActive(): boolean {
    return this.filterValues?.some(x => x.selected) || this.displayEmptyFields;
  }

  public getModel(): any {
    return this.isFilterActive() ? {
      filterType: this.filterType, filter: this.filterValues, relationIdFieldName: this.relationIdFieldName
    } : null;
  }

  public resetFilter(): void {
    this.filterValues?.forEach(filterValue => filterValue.selected = false);
    this.displayEmptyFields = false;
  }

  public removeFilterValue(filterValue: ValueSelected): void {
    filterValue.selected = false;
  }

  public async setupContentGui(): Promise<string> {
    return '';
  }

  public async loadValues() {
    if (this.isDestroyed()) {
      // Destroyed
      return;
    }
    this.dispose();
    let oldFilterValues = this.filterValues;
    await this.getValues();
    if (oldFilterValues && oldFilterValues.length > 0) {
      oldFilterValues.forEach(oldFilterValue => {
        if (this.filterValues && this.filterValues.some(x => x.name == oldFilterValue.name)) {
          this.filterValues.find(x => x.name == oldFilterValue.name).selected = oldFilterValue.selected;
        }
      });
    }
  }

  public async getValues() {
    this.filterValues = new Array<ValueSelected>();
    this.dispose();
    this.originalValues = this.service != null ? await this.service.getEntities(this.service.gridDataSource.predicates, this.service.gridDataSource.expands, { ...this.service.gridDataSource.queryParameters, $inlinecount: 'allpages' }) : [];
    this.setFilterValues(this.originalValues);

    this.applyObservers();
  }

  private setFilterValues(values: T[]) {
    this.ignoreChanges = true;
    this.filterValues.splice(0);
    values.forEach((value: T) => {
      let obj = this.filterValues.find(x => x.id == value["id"]);
      if (obj == null) {
        obj = { id: value["id"], name: this.getFieldValue(value), selected: false, hide: false };
        this.filterValues.push(obj);
      }
    });
    this.filterValues.sortBy("name");
    this.ignoreChanges = false;
  }

  protected applyObservers() {
    this.dispose(); // Making sure that everything is disposed before re-adding observers
    this.filterValues?.forEach(obj =>
      this.disposables.push(this.bindingEngine.propertyObserver(obj, 'selected').subscribe(this.filterValuesChanged.bind(this)))
    );
    this.selectedIdsObserver = this.bindingEngine.collectionObserver(this.selectedIds).subscribe((changeRecords) => {
      if (!this.selectedIdInternalChange) {
        if (changeRecords?.length > 0 && changeRecords[0].removed.length > 0) {
          changeRecords[0].removed.forEach(id => {
            let value = this.filterValues.find(x => x.id == id);
            if (value != null) {
              value.selected = false;
            }
          });
        }
        this.selectedIds.forEach(id => {
          let value = this.filterValues.find(x => x.id == id);
          if (value != null) {
            value.selected = true;
          }
        })
      }
    });
    this.disposables.push(this.bindingEngine.propertyObserver(this, 'displayEmptyFields').subscribe((newValue, oldValue) => this.filterValuesChanged(newValue, oldValue)));
  }

  protected detectAndSetRelationIdFieldName(params: IFilterParams & { relationIdFieldName: string }) {
    if (params.relationIdFieldName != null) {
      this.relationIdFieldName = params.relationIdFieldName;
    }
    else {
      console.warn(`Grid column ${params.colDef?.field} - relationIdFieldName parameter is missing. You should use the relationIdFieldName parameter to specify the name of the field containing the id of the relation, for better performance.`);
    }
  }

  public getFieldValue(value: T) {
    let fieldValue: any = value;
    this.fieldPath.forEach(fieldName => {
      const newFieldValue = fieldValue[fieldName];
      if (newFieldValue != null) {
        fieldValue = newFieldValue;
      }
    });
    return fieldValue as string;
  }

  public filterValuesChanged(newVal: boolean, oldVal: boolean) {
    if (newVal != oldVal && !this.firstTime && !this.ignoreChanges) {
      this.setSelectedValues();
      const instance = this.params.api.getFilterInstance((this.params as IFilterParams).colDef.field);
      instance.setModel(Object.assign(
        { filterType: this.filterType, filter: this.filterValues },
        this.extendFilterValuesChangesModel()
      ));
    }
  }

  protected extendFilterValuesChangesModel(): any {
    return {};
  }

  protected setSelectedValues() {
    this.selectedIdInternalChange = true;
    this.filterValuesSelected.splice(0);
    this.filterValuesSelected.push(...this.filterValues.filter(x => x.selected));
    this.selectedIds.splice(0);
    this.selectedIds.push(...this.filterValuesSelected.map(x => x.id));
    this.DROPDOWN_NAME = this.getModelAsString();
    this.taskQueue.queueMicroTask(() => this.selectedIdInternalChange = false);
  }

  public setModel(model: any): void {
    this.currentSetModelRetries++;
    if (this.currentSetModelRetries > MAX_SET_MODEL_RETRIES) {
      console.error(`Grid column ${this.fieldPath} - Max retries reached, cannot set model. Filters and views may be out of order.`);
      return;
    }

    if (this.originalValues == null) {
      console.warn(`Grid column ${this.fieldPath} - OriginalValues not loaded, cannot reset filter. Current retry counter is ${this.currentSetModelRetries}/${MAX_SET_MODEL_RETRIES}. Try again in 200ms.`);
      setTimeout(() => this.setModel(model), 200);
    }
    else {
      this.currentSetModelRetries = 0;
      this.internalSetModel(model);
    }
  }

  private internalSetModel(model: any): void {
    if (model == null) {
      if (this.originalValues == null) {
        console.error("Original values are null, cannot reset filter");
      }
      if (this.isDestroyed()) {
        this.dispose();
        this.filterValues = [];
      } else {
        this.resetFilter();
      }
    } else if (model.filterType == this.filterType) {
      // Deep copy the model filter to be sure we don't change the provided filter
      const filterCopy = JSON.parse(JSON.stringify(model.filter));
      this.dispose();
      this.ignoreChanges = true;
      this.filterValues?.forEach(filterValue => filterValue.selected = false);

      filterCopy.forEach(fromModelValue => {
        let id = fromModelValue.id;
        let filterValue = this.filterValues.find(x => x.id == id);
        if (filterValue != null) {
          filterValue.selected = fromModelValue.selected;
        }
        else {
          console.warn("Original value not found for id " + id);
        }
      });

      this.applyObservers();
      this.setModelHook(model);
      setTimeout(() => {
        this.ignoreChanges = false;
      }, 0);

    }
    this.setSelectedValues();
    this.filterChangedCallback();
  }

  protected setModelHook(model: any) {

  }

  public getValue(): Array<ValueSelected> {
    return this.filterValuesSelected;
  }

  public getModelAsString?(): string {
    return '';
  }

  protected dispose(): void {
    super.dispose();
    this.disposables.splice(0);
    this.selectedIdsObserver?.dispose();
    this.selectedIdsObserver = null;
  }

  protected buildService(params: IFilterParams<any> & { serviceName: string; service: ServiceBase<T>; }) {
    if (params.service != null) {
      this.service = params.service;
    } else if (params.serviceName != null) {
      this.service = new ServiceBase<T>(params.serviceName);
    }
    else if (params.colDef.floatingFilterComponentParams.service != null) {
      this.service = params.colDef.floatingFilterComponentParams.service;
    }
    else if (params.colDef.floatingFilterComponentParams.serviceName != null) {
      this.service = new ServiceBase<T>(params.colDef.floatingFilterComponentParams.serviceName);
    }

    if (this.service == null && !this.config.agGridConfig.suppressMenu) {
      throw new Error("service or serviceName parameter is missing");
    }
  }
}