'use strict';

/**
 * Implements a queue structure
 * @template T Queue values type
 */
export default class Queue<T> {
  
  private _length: number;
  private _start: any;
  private _end: any;

  /**
   * Creates a queue from array
   * @param {Array<V>} array Array to create the queue from
   * @return {Queue<V>} Queue created from the array
   * @deprecated Use constructor instead
   */
  static fromArray<V>(array: V[]) {
    let queue = new Queue<V>();
    array.forEach(item => queue.push(item));
    return queue;
  }

  /**
   * Constructs instance
   * @param {Iterable<T>} [items] Initial items to add to the queue
   */
  constructor(items?: Iterable<T>) {
    for (let item of items || []) {
      this.push(item);
    }
    this._length = 0;
  }

  /**
   * Returns the queue size
   * @return {Number} Queue size
   */
  get length() {
    return this._length;
  }

  /**
   * Iterates by queue items
   */
  *[Symbol.iterator](): Iterator<T> {
    let current = this._start;
    while (current) {
      yield current.value;
      current = current.next;
    }
  }

  /**
   * Returns the first element or undefined if the queue is empty
   * @return {T} The first value
   */
  front(): T | undefined {
    return this._start?.value;
  }

  /**
   * Returns the last element or undefined if the queue is empty
   * @return {T} The last value
   */
  back(): T | undefined {
    return this._end?.value;
  }

  /**
   * Pushes new elements to the end of the queue
   * @param {T[]} values Values to push
   */
  push(...values: T[]) {
    for (let value of values) {
      if (this._end) {
        this._end.next = {value, prev: this._end};
        this._end = this._end.next;
      } else {
        this._start = this._end = {value};
      }
      this._length++;
    }
  }

  /**
   * Removes the last element from the queue
   * @return {T} The value removed or undefined
   */
  pop(): T | undefined {
    if (this._end) {
      let result = this._end;
      this._remove(this._end);
      return result.value;
    }
  }

  /**
   * Inserts new elements to the front of the queue
   * @param {T} value Values to unshift
   */
  unshift(value: T) {
    if (this._start) {
      this._start = {value, next: this._start};
      this._start.next.prev = this._start;
    } else {
      this._start = this._end = {value};
    }
    this._length++;
  }

  /**
   * Removes the first element from the queue
   * @return {T} The value removed or undefined
   */
  shift(): T | undefined {
    if (this._start) {
      let result = this._start;
      this._remove(this._start);
      return result.value;
    }
  }

  /**
   * Finds an element in the queue by the predicate
   * @param {(value: T) => boolean} predicate Predicate to use
   * @return {T} Element value found or undefined
   */
  find(predicate: (value: T) => boolean): T | undefined {
    let current = this._start;
    while (current) {
      if (predicate(current.value)) {
        return current.value;
      }
      current = current.next;
    }
  }

  /**
   * Iterates over the queue
   * @param {(value: T, index: Number) => any} callable Iterator callable
   */
  forEach(callable: (value: T, index: number) => any) {
    let current = this._start;
    let index = 0;
    while (current) {
      callable(current.value, index++);
      current = current.next;
    }
  }

  /**
   * Removes elements iterating them and matching by predicate
   * @param {(value: T) => boolean|null} predicate Returns true for values to remove. If returns null, iteration stops
   */
  remove(predicate: (value: T) => boolean | null) {
    let current = this._start;
    while (current) {
      let remove = predicate(current.value);
      if (remove === null) {
        break;
      }
      if (remove === true) {
        this._remove(current);
      }
      current = current.next;
    }
  }

  /**
   * Removes the first occurence of element matched by predicate
   * @param predicate returns true for value to removed
   */
  removeOne(predicate: (value: T) => boolean) {
    let removed = false;
    this.remove(item => {
      if (removed) {
        return null;
      }
      if (predicate(item)) {
        removed = true;
        return true;
      }
    });
  }

  /**
   * Creates a new queue from this one in reversed order
   * @return {Queue<T>} Reversed queue
   */
  reversed(): Queue<T> {
    let result = new Queue<T>();
    let current = this._end;
    while (current) {
      result.push(current.value);
      current = current.prev;
    }
    return result;
  }

  /**
   * Creates an array from the queue
   * @return {Array<T>} Array created
   */
  toArray(): T[] {
    let result = [];
    this.forEach(value => result.push(value));
    return result;
  }

  /**
   * Clears the queue
   */
  clear() {
    delete this._start;
    delete this._end;
    this._length = 0;
  }

  private _remove(node) {
    if (node.prev) {
      node.prev.next = node.next;
    } else {
      this._start = node.next;
    }
    if (node.next) {
      node.next.prev = node.prev;
    } else {
      this._end = node.prev;
    }
    this._length--;
  }
}
