/*
 * Copyright (c) 2010, 2024 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 {arrays, HierarchicalLookupResultBuilder, InitModelOf, LookupCall, LookupResult, LookupRow, objects, QueryBy, scout, StaticLookupCallModel, strings} from '../index';
import $ from 'jquery';

/**
 * Base class for lookup calls with static or local data. Implement the _data() and _dataToLookupRow()
 * functions to provide data for lookup calls. Results are resolved as a Promise, the delay
 * property controls how long it takes until the promise is resolved. You can set it to a higher value for testing purposes.
 */
export class StaticLookupCall<TKey> extends LookupCall<TKey> implements StaticLookupCallModel<TKey> {
  declare model: StaticLookupCallModel<TKey>;

  delay: number;
  data: any[];
  protected _deferred: JQuery.Deferred<LookupResult<TKey>>;

  constructor() {
    super();
    this._deferred = null;
    this.delay = 0;
    this.data = null;
    this.active = true;
  }

  protected override _init(model: InitModelOf<this>) {
    super._init(model);
    if (!this.data) {
      // data may either be provided by the model or by implementing the _data function
      this.data = this._data();
    }
  }

  refreshData(data?: any[]) {
    if (data === undefined) {
      this.data = this._data();
    } else {
      this.data = data;
    }
  }

  override abort() {
    this._deferred?.reject({
      abort: true
    });
    super.abort();
  }

  protected override _getAll(): JQuery.Promise<LookupResult<TKey>> {
    this._deferred = $.Deferred();
    setTimeout(this._queryByAll.bind(this), this.delay);
    return this._deferred.promise();
  }

  protected _queryByAll() {
    this._deferred.resolve({
      queryBy: QueryBy.ALL,
      lookupRows: this._lookupRowsByAll()
    });
  }

  /**
   * Limits the data to 'maxRowCount' results, converts the data to LookupRows and filters
   * the rows by their 'active' state. If a subclass overrides this method it can omit the
   * super call, if it must avoid the maxRowCount limit and does its own filtering. In that
   * case the subclass may choose to call <code>#_mapAndFilterData</code> and
   * <code>#_limitMaxRows</code> when suitable.
   */
  protected _lookupRowsByAll(): LookupRow<TKey>[] {
    let datas = this._limitMaxRows(this.data);
    return this._mapAndFilterData(datas);
  }

  protected _limitMaxRows(array: any[]): any[] {
    return arrays.ensure(array).slice(0, this.maxRowCount);
  }

  protected _mapAndFilterData(datas: any[]): LookupRow<TKey>[] {
    return datas
      .map(this._dataToLookupRow, this)
      .filter(this._filterActiveLookupRow, this);
  }

  protected _filterActiveLookupRow(dataRow: LookupRow<TKey>): boolean {
    if (objects.isNullOrUndefined(this.active)) {
      return true;
    }
    return this.active === scout.nvl(dataRow.active, true);
  }

  protected override _getByText(text: string): JQuery.Promise<LookupResult<TKey>> {
    this._deferred = $.Deferred();
    setTimeout(this._queryByText.bind(this, text), this.delay);
    return this._deferred.promise();
  }

  protected _queryByText(text: string) {
    let lookupRows = this._lookupRowsByText(text);

    // resolve non-hierarchical results immediately
    if (!this.hierarchical) {
      this._deferred.resolve({
        queryBy: QueryBy.TEXT,
        text: text,
        lookupRows: lookupRows
      });
    }

    // if loadIncremental=false we must also load children
    let promise, builder = new HierarchicalLookupResultBuilder(this);
    if (this.loadIncremental) {
      promise = $.resolvedPromise(lookupRows);
    } else {
      promise = builder.addChildLookupRows(lookupRows);
    }

    // hierarchical lookups must first load their parent nodes
    // before we can resolve the results
    promise
      .then(lookupRows => builder.addParentLookupRows(lookupRows))
      .done(lookupRows => {
        this._deferred.resolve({
          queryBy: QueryBy.TEXT,
          text: text,
          lookupRows: lookupRows
        });
      })
      .fail(error => {
        throw error;
      });
  }

  protected _lookupRowsByText(text: string): LookupRow<TKey>[] {
    let regex = this._createSearchPattern(text);
    let datas = this.data.filter(data => regex.test(data[1].toLowerCase()));
    return this._mapAndFilterData(datas);
  }

  _createSearchPattern(text: string): RegExp {
    // Implementation copied from LocalLookupCall.java

    const WILDCARD = '*';
    const WILDCARD_PLACEHOLDER = '@wildcard@';

    text = strings.nvl(text);
    text = text.toLowerCase();
    text = text.replace(new RegExp(strings.quote(WILDCARD), 'g'), WILDCARD_PLACEHOLDER);
    text = strings.quote(text);

    // replace repeating wildcards to prevent regex DoS
    let duplicateWildcards = WILDCARD_PLACEHOLDER + WILDCARD_PLACEHOLDER;
    while (strings.contains(text, duplicateWildcards)) {
      text = text.replace(duplicateWildcards, WILDCARD_PLACEHOLDER);
    }

    if (!strings.endsWith(text, WILDCARD_PLACEHOLDER)) {
      text += WILDCARD_PLACEHOLDER;
    }

    text = text.replace(new RegExp(strings.quote(WILDCARD_PLACEHOLDER), 'g'), '.*');

    return new RegExp('^' + text + '$', 's'); // s = DOT_ALL
  }

  protected override _getByKey(key: TKey): JQuery.Promise<LookupResult<TKey>> {
    this._deferred = $.Deferred();
    setTimeout(this._queryByKey.bind(this, key), this.delay);
    return this._deferred.promise();
  }

  protected _queryByKey(key: TKey) {
    let lookupRow = this._lookupRowByKey(key);
    if (lookupRow) {
      this._deferred.resolve({
        queryBy: QueryBy.KEY,
        lookupRows: [lookupRow]
      });
    } else {
      this._deferred.reject();
    }
  }

  protected override _getByKeys(keys: TKey[]): JQuery.Promise<LookupResult<TKey>> {
    this._deferred = $.Deferred();
    setTimeout(() => this._queryByKeys(keys), this.delay);
    return this._deferred.promise();
  }

  protected _queryByKeys(keys: TKey[]) {
    const lookupRows = arrays.ensure(keys).map(key => this._lookupRowByKey(key)).filter(row => !!row);
    if (lookupRows.length) {
      this._deferred.resolve({
        queryBy: QueryBy.KEY,
        lookupRows
      });
    } else {
      this._deferred.reject();
    }
  }

  protected _lookupRowByKey(key: TKey): LookupRow<TKey> {
    let data = arrays.find(this.data, data => data[0] === key);
    if (!data) {
      return null;
    }
    return this._dataToLookupRow(data);
  }

  protected override _getByRec(rec: TKey): JQuery.Promise<LookupResult<TKey>> {
    this._deferred = $.Deferred();
    setTimeout(this._queryByRec.bind(this, rec), this.delay);
    return this._deferred.promise();
  }

  protected _queryByRec(rec: TKey) {
    this._deferred.resolve({
      queryBy: QueryBy.REC,
      rec: rec,
      lookupRows: this._lookupRowsByRec(rec)
    });
  }

  protected _lookupRowsByRec(rec: TKey): LookupRow<TKey>[] {
    return this.data.reduce((aggr, data) => {
      if (data[2] === rec) {
        aggr.push(this._dataToLookupRow(data));
      }
      return aggr;
    }, [])
      .filter(this._filterActiveLookupRow, this);
  }

  setDelay(delay: number) {
    this.delay = delay;
  }

  /**
   * Implement this function to convert a single data array into an instance of LookupRow.
   */
  protected _dataToLookupRow(data: any[], index?: number): LookupRow<TKey> {
    return scout.create(LookupRow, {
      key: data[0],
      text: data[1],
      parentKey: data[2]
    }) as LookupRow<TKey>;
  }

  /**
   * Implement this function to provide static data.
   *
   * The result is expected to be a list of tuples. Each tuple will be converted to a lookup row via
   * {@link _dataToLookupRow}. The first three elements of each tuple have a predefined meaning and
   * are used by the lookup call (e.g. to filter rows by text). More elements can be added freely.
   *
   * - [0] = key
   * - [1] = text
   * - [2] = parentKey
   */
  protected _data(): any[] {
    return [];
  }
}
