import { max, min, isArray } from '../alias';
import { Index } from '../index';

// Circular Indexed List

export class List<T> {
  constructor(
    public capacity: number = 0,
    private readonly Container: ArrayConstructor | Uint32ArrayConstructor = Array,
  ) {
    if (capacity <= 0) {
      capacity = -capacity || 4;
      this.auto = true;
    }
    assert(this.capacity > 0);
    if (capacity >= 2 ** 32) throw new Error(`Too large capacity`);
    this.capacity = capacity;
    this.values = new this.Container(this.capacity) as T[];
    this.nexts = new Uint32Array(this.capacity);
    this.prevs = new Uint32Array(this.capacity);
  }
  private auto = false;
  private values: T[];
  private nexts: Uint32Array;
  private prevs: Uint32Array;
  private readonly ix = new Index();
  private $length = 0;
  public get length() {
    return this.$length;
  }
  public head = 0;
  public get tail(): number {
    return this.nexts[this.head];
  }
  public get last(): number {
    return this.prevs[this.head];
  }
  public next(index: number): number {
    return this.nexts[index];
  }
  public prev(index: number): number {
    return this.prevs[index];
  }
  public at(offset: number): T {
    return this.values[this.index(offset)];
  }
  public index(offset: number, index = this.head): number {
    switch (offset) {
      case 0:
        return index;
      case 1:
        return this.nexts[index];
      case -1:
        return this.prevs[index];
    }
    if (offset > 0) {
      const pointers = this.nexts;
      while (offset-- !== 0) {
        index = pointers[index];
      }
    }
    else {
      const pointers = this.prevs;
      while (offset++ !== 0) {
        index = pointers[index];
      }
    }
    return index;
  }
  public resize(capacity: number): void {
    if (capacity <= 0) {
      capacity = -capacity || 4;
      this.auto = true;
    }
    assert(this.capacity > 0);
    if (capacity > 2 ** 32) throw new Error(`Too large capacity`);
    if (capacity > this.nexts.length) {
      const size = max(capacity, min(this.capacity * 2, 2 ** 32));
      if (isArray(this.values)) {
        this.values.length = size;
      }
      else {
        const values = new this.Container(size) as Uint32Array;
        values.set(this.values);
        this.values = values as any;
      }
      const nexts = new Uint32Array(size);
      nexts.set(this.nexts);
      this.nexts = nexts;
      const prevs = new Uint32Array(size);
      prevs.set(this.prevs);
      this.prevs = prevs;
    }
    this.capacity = capacity;
    while (this.$length > capacity) {
      this.del(this.last);
    }
  }
  public clear(): void {
    this.values = new this.Container(this.capacity) as T[];
    this.ix.clear();
    this.head = 0;
    this.$length = 0;
  }
  public add(value: T): number {
    const head = this.head;
    if (this.$length === 0) {
      assert(this.length === 0);
      const index = this.head = this.ix.pop();
      ++this.$length;
      this.values[index] = value;
      this.nexts[index] = index;
      this.prevs[index] = index;
      return index;
    }
    if (this.$length !== this.capacity) {
      assert(this.length < this.capacity);
      const index = this.head = this.ix.pop();
      ++this.$length;
      const last = this.prevs[head];
      this.prevs[head] = this.nexts[last] = index;
      this.values[index] = value;
      this.nexts[index] = head;
      this.prevs[index] = last;
      return index;
    }
    else if (!this.auto) {
      assert(this.length === this.capacity);
      assert(this.ix.length === this.capacity);
      const index = this.head = this.prevs[head];
      this.values[index] = value;
      return index;
    }
    else {
      this.resize(max(min(this.capacity * 2, 2 ** 32), this.capacity + 1));
      return this.add(value);
    }
  }
  public del(index: number): void {
    assert(this.length > 0);
    const next = this.nexts[index];
    const prev = this.prevs[index];
    this.ix.push(index);
    this.values[index] = undefined as any;
    if (--this.$length !== 0) {
      this.nexts[prev] = next;
      this.prevs[next] = prev;
    }
    if (index === this.head) {
      this.head = next;
    }
  }
  public get(index: number): T {
    return this.values[index];
  }
  public set(index: number, value: T): void {
    this.values[index] = value;
  }
  public insert(value: T, before: number): number {
    const head = this.head;
    this.head = before;
    const index = this.add(value);
    this.head = head;
    return index;
  }
  public unshift(value: T): number {
    return this.add(value);
  }
  public unshiftRotationally(value: T): number {
    if (this.$length === 0) return this.add(value);
    const index = this.last;
    this.values[index] = value;
    this.head = index;
    return index;
  }
  public push(value: T): number {
    return this.insert(value, this.head);
  }
  public pushRotationally(value: T): number {
    if (this.$length === 0) return this.add(value);
    const index = this.head;
    this.values[index] = value;
    this.head = this.nexts[index];
    return index;
  }
  public shift(): T | undefined {
    if (this.$length === 0) return;
    const index = this.head;
    const value = this.values[index];
    this.del(index);
    return value;
  }
  public pop(): T | undefined {
    if (this.$length === 0) return;
    const index = this.last;
    const value = this.values[index];
    this.del(index);
    return value;
  }
  public move(index: number, before: number): boolean {
    if (index === before) return false;
    const a1 = index;
    const b1 = before;
    assert(a1 !== b1);
    const { nexts, prevs } = this;
    const a2 = nexts[a1];
    if (a2 === b1) return false;
    const b0 = prevs[b1];
    const a0 = prevs[a1];
    nexts[b0] = a1;
    nexts[a1] = b1;
    prevs[b1] = a1;
    prevs[a1] = b0;
    nexts[a0] = a2;
    prevs[a2] = a0;
    assert(this.length > 10 || [...this].length === this.length);
    return true;
  }
  public moveToHead(index: number): void {
    this.move(index, this.head);
    this.head = index;
  }
  public moveToLast(index: number): void {
    this.move(index, this.head);
    this.head = index === this.head
      ? this.tail
      : this.head;
  }
  public swap(index1: number, index2: number): boolean {
    if (index1 === index2) return false;
    const index3 = this.nexts[index2];
    this.move(index2, index1);
    this.move(index1, index3);
    switch (this.head) {
      case index1:
        this.head = index2;
        break;
      case index2:
        this.head = index1;
        break;
    }
    return true;
  }
  public *[Symbol.iterator](): Iterator<T, undefined, undefined> {
    for (let index = this.head; this.$length !== 0;) {
      yield this.values[index];
      index = this.nexts[index];
      if (index === this.head) break;
    }
  }
}
