/**
 * Copyright (c) 2022 The xterm.js authors. All rights reserved.
 * @license MIT
 */

import { IdleTaskQueue } from './TaskQueue';
import type { ILogService } from './services/Services';

// Work variables to avoid garbage collection.
let i = 0;

/**
 * A generic list that is maintained in sorted order and allows values with duplicate keys. Deferred
 * batch insertion and deletion is used to significantly reduce the time it takes to insert and
 * delete a large amount of items in succession. This list is based on binary search and as such
 * locating a key will take O(log n) amortized, this includes the by key iterator.
 */
export class SortedList<T> {
  private _array: T[] = [];

  private readonly _insertedValues: T[] = [];
  private readonly _flushInsertedTask: InstanceType<typeof IdleTaskQueue>;
  private _isFlushingInserted = false;

  private readonly _deletedIndices: number[] = [];
  private readonly _flushDeletedTask: InstanceType<typeof IdleTaskQueue>;
  private _isFlushingDeleted = false;

  constructor(
    private readonly _getKey: (value: T) => number,
    logService: ILogService
  ) {
    this._flushInsertedTask = new IdleTaskQueue(logService);
    this._flushDeletedTask = new IdleTaskQueue(logService);
  }

  public clear(): void {
    this._array.length = 0;
    this._insertedValues.length = 0;
    this._flushInsertedTask.clear();
    this._isFlushingInserted = false;
    this._deletedIndices.length = 0;
    this._flushDeletedTask.clear();
    this._isFlushingDeleted = false;
  }

  public insert(value: T): void {
    this._flushCleanupDeleted();
    if (this._insertedValues.length === 0) {
      this._flushInsertedTask.enqueue(() => this._flushInserted());
    }
    this._insertedValues.push(value);
  }

  private _flushInserted(): void {
    const sortedAddedValues = this._insertedValues.sort((a, b) => this._getKey(a) - this._getKey(b));
    let sortedAddedValuesIndex = 0;
    let arrayIndex = 0;

    const newArray = new Array(this._array.length + this._insertedValues.length);

    for (let newArrayIndex = 0; newArrayIndex < newArray.length; newArrayIndex++) {
      if (arrayIndex >= this._array.length || this._getKey(sortedAddedValues[sortedAddedValuesIndex]) <= this._getKey(this._array[arrayIndex])) {
        newArray[newArrayIndex] = sortedAddedValues[sortedAddedValuesIndex];
        sortedAddedValuesIndex++;
      } else {
        newArray[newArrayIndex] = this._array[arrayIndex++];
      }
    }

    this._array = newArray;
    this._insertedValues.length = 0;
  }

  private _flushCleanupInserted(): void {
    if (!this._isFlushingInserted && this._insertedValues.length > 0) {
      this._flushInsertedTask.flush();
    }
  }

  public delete(value: T): boolean {
    this._flushCleanupInserted();
    if (this._array.length === 0) {
      return false;
    }
    const key = this._getKey(value);
    if (key === undefined) {
      return false;
    }
    i = this._search(key);
    if (i === -1) {
      return false;
    }
    if (this._getKey(this._array[i]) !== key) {
      return false;
    }
    do {
      if (this._array[i] === value) {
        if (this._deletedIndices.length === 0) {
          this._flushDeletedTask.enqueue(() => this._flushDeleted());
        }
        this._deletedIndices.push(i);
        return true;
      }
    } while (++i < this._array.length && this._getKey(this._array[i]) === key);
    return false;
  }

  private _flushDeleted(): void {
    this._isFlushingDeleted = true;
    const sortedDeletedIndices = this._deletedIndices.sort((a, b) => a - b);
    let sortedDeletedIndicesIndex = 0;
    const newArray = new Array(this._array.length - sortedDeletedIndices.length);
    let newArrayIndex = 0;
    for (let i = 0; i < this._array.length; i++) {
      if (sortedDeletedIndices[sortedDeletedIndicesIndex] === i) {
        sortedDeletedIndicesIndex++;
      } else {
        newArray[newArrayIndex++] = this._array[i];
      }
    }
    this._array = newArray;
    this._deletedIndices.length = 0;
    this._isFlushingDeleted = false;
  }

  private _flushCleanupDeleted(): void {
    if (!this._isFlushingDeleted && this._deletedIndices.length > 0) {
      this._flushDeletedTask.flush();
    }
  }

  public *getKeyIterator(key: number): IterableIterator<T> {
    this._flushCleanupInserted();
    this._flushCleanupDeleted();
    if (this._array.length === 0) {
      return;
    }
    i = this._search(key);
    if (i < 0 || i >= this._array.length) {
      return;
    }
    if (this._getKey(this._array[i]) !== key) {
      return;
    }
    do {
      yield this._array[i];
    } while (++i < this._array.length && this._getKey(this._array[i]) === key);
  }

  public forEachByKey(key: number, callback: (value: T) => void): void {
    this._flushCleanupInserted();
    this._flushCleanupDeleted();
    if (this._array.length === 0) {
      return;
    }
    i = this._search(key);
    if (i < 0 || i >= this._array.length) {
      return;
    }
    if (this._getKey(this._array[i]) !== key) {
      return;
    }
    do {
      callback(this._array[i]);
    } while (++i < this._array.length && this._getKey(this._array[i]) === key);
  }

  public values(): IterableIterator<T> {
    this._flushCleanupInserted();
    this._flushCleanupDeleted();
    // Duplicate the array to avoid issues when _array changes while iterating
    return [...this._array].values();
  }

  private _search(key: number): number {
    let min = 0;
    let max = this._array.length - 1;
    while (max >= min) {
      let mid = (min + max) >> 1;
      const midKey = this._getKey(this._array[mid]);
      if (midKey > key) {
        max = mid - 1;
      } else if (midKey < key) {
        min = mid + 1;
      } else {
        // key in list, walk to lowest duplicate
        while (mid > 0 && this._getKey(this._array[mid - 1]) === key) {
          mid--;
        }
        return mid;
      }
    }
    // key not in list
    // still return closest min (also used as insert position)
    return min;
  }
}
