import { GArray } from 'godot'

const DoneIteratorResult = Object.freeze({ value: undefined, done: true })

type Nullable<T> = T | null | undefined

export interface Predicate<T> {
  (value: Nullable<T>, index: number): boolean
}

export interface Reducer<T, U = T> {
  (accumulator: U, value: Nullable<T>, index: number): U
}

export interface Morphism<T, U = T> {
  (value: Nullable<T>, index: number): U
}

interface IEnumeratorFactory<T> {
  (iterator: Iterator<T>): IEnumerator<T>
}

export interface IEnumerator<T> {
  get current(): Nullable<T>
  moveNext(): boolean
  reset(): void
  toIterator<T>(): Iterator<T>
}

export interface IEnumerable<T> {
  enumerator(): IEnumerator<T>
}

export function isIterable(value: any): value is Iterable<any> {
  return typeof value[Symbol.iterator] === 'function'
}

export function isIterator(value: any): value is Iterator<any> {
  return typeof value.next === 'function'
}

export function isBuffer(value: any): value is ArrayBufferLike {
  return ArrayBuffer.isView(value)
}

export function isGArray(value: any): value is GArray {
  return typeof value.get_indexed === 'function'
}

export function isArrayLike(value: any): value is ArrayLike<any> {
  return typeof value.length === 'number'
}

class IteratorEnumerator<T> implements IEnumerator<T>, Iterator<T> {
  private _iterator: Iterator<T>
  private _consumed: boolean = false
  private _current: Nullable<T>

  get current(): Nullable<T> {
    return this._current
  }

  constructor(iterator: Iterator<T>) {
    this._iterator = iterator
  }

  static from<T>(iterator: Iterator<T>): IteratorEnumerator<T> {
    return new IteratorEnumerator(iterator)
  }

  moveNext(): boolean {
    if (!this._consumed) {
      const { value, done } = this._iterator.next()

      this._current = value
      if (done) {
        this._consumed = true
      }

      return !done
    } else {
      return false
    }
  }

  reset(): void { }

  toIterator<T>(): Iterator<T> {
    return this as Iterator<T>
  }

  next(...[_value]: [] | [any]): IteratorResult<T> {
    const done = this.moveNext()
    return { value: this.current, done } as IteratorResult<T>
  }

  return(value?: any): IteratorResult<T, any> {
    return this._iterator.return?.(value) ?? DoneIteratorResult
  }

  throw(e?: any): IteratorResult<T, any> {
    return this._iterator.throw?.(e) ?? DoneIteratorResult
  }
}

class CachedIteratorEnumerator<T> implements IEnumerator<T>, Iterator<T> {
  private _iterator: IteratorEnumerator<T>
  private _cache: Nullable<T>[] = []
  private _index: number = -1

  get current(): Nullable<T> {
    return this._cache[this._index]
  }

  constructor(iterator: Iterator<T>) {
    this._iterator = new IteratorEnumerator(iterator)
  }

  static from<T>(iterator: Iterator<T>): CachedIteratorEnumerator<T> {
    return new CachedIteratorEnumerator(iterator)
  }

  moveNext(): boolean {
    this._index += 1

    if (this._cache.length > this._index) {
      return true
    } else if (!this._iterator.moveNext()) {
      this._cache.push(this._iterator.current)

      return !true
    } else {
      return false
    }
  }

  reset(): void {
    this._index = -1
  }

  toIterator<T>(): Iterator<T> {
    return this as Iterator<T>
  }

  next(...[_value]: [] | [any]): IteratorResult<T> {
    const done = this.moveNext()
    return { value: this.current, done } as IteratorResult<T>
  }

  return?(value?: any): IteratorResult<T, any> {
    return this._iterator.return(value)
  }

  throw(e?: any): IteratorResult<T, any> {
    return this._iterator.throw(e)
  }
}

export class IterableEnumerator<T> implements IEnumerator<T>, Iterator<T> {
  private _iterable: Iterable<T>
  private _factory: IEnumeratorFactory<T>
  private _enumerator: Nullable<IEnumerator<T>>

  get current(): Nullable<T> {
    return this._enumerator?.current
  }

  constructor(iterable: Iterable<T>, factory: IEnumeratorFactory<T> = IteratorEnumerator.from) {
    this._iterable = iterable
    this._factory = factory
  }

  static fromIterable<T>(iterable: Iterable<T>, factory?: IEnumeratorFactory<T>): IEnumerator<T> {
    return new this(iterable, factory)
  }

  moveNext(): boolean {
    return this._enumerator?.moveNext() ?? false
  }

  _createIterator(): Iterator<T> {
    return this._iterable[Symbol.iterator]()
  }

  _createEnumerator(): IEnumerator<T> {
    return this._factory(this._createIterator())
  }

  reset(): void {
    this._enumerator = this._createEnumerator()
  }

  toIterator<T>(): Iterator<T> {
    return this._createIterator() as Iterator<T>
  }

  next(...[_value]: [] | [any]): IteratorResult<T, any> {
    const done = !this.moveNext()
    return { value: this.current, done } as IteratorResult<T, any>
  }

  return?(value?: any): IteratorResult<T, any> {
    if (isIterator(this._enumerator)) {
      return this._enumerator?.return?.(value) || DoneIteratorResult
    } else {
      return DoneIteratorResult
    }
  }

  throw?(e?: any): IteratorResult<T, any> {
    if (isIterator(this._enumerator)) {
      return this._enumerator?.throw?.(e) || DoneIteratorResult
    } else {
      return DoneIteratorResult
    }
  }
}

class Enumerator<T> implements IEnumerator<T>, Iterator<T> {
  protected _enumerator: IEnumerator<T>
  protected _index: number = 0

  get current(): Nullable<T> {
    return this._enumerator.current
  }

  constructor(enumerator: IEnumerator<T>) {
    this._enumerator = enumerator
  }

  static fromIterable<T>(iterable: Iterable<T>): IEnumerator<T> {
    return new this(new IterableEnumerator(iterable))
  }

  static fromIterator<T>(iterator: Iterator<T>, cache: boolean = true): IEnumerator<T> {
    if (cache) {
      return new CachedIteratorEnumerator(iterator)
    } else {
      return new IteratorEnumerator(iterator)
    }
  }

  static toIterator<T>(enumerator: IEnumerator<T>): Iterator<T> {
    return new this(enumerator)
  }

  [Symbol.iterator]() {
    return this.toIterator()
  }

  toIterator<T>(): Iterator<T> {
    return Enumerator.toIterator(this) as Iterator<T>
  }

  moveNext(): boolean {
    this._index += 1
    return this._enumerator.moveNext()
  }

  reset(): void {
    this._enumerator.reset()
  }

  next(...[_value]: [] | [any]): IteratorResult<T, any> {
    const done = this.moveNext()

    return { value: this.current, done } as IteratorResult<T>
  }

  return?(_value?: any): IteratorResult<T, any> {
    return DoneIteratorResult
  }

  throw?(_e?: any): IteratorResult<T, any> {
    return DoneIteratorResult
  }
}

class DropEnumerator<T> extends Enumerator<T> {
  private _limit: number

  constructor(enumerator: IEnumerator<T>, limit: number) {
    super(enumerator)
    this._limit = limit
  }

  moveNext(): boolean {
    for (let i = 0; i < this._limit; i++) {
      if (!super.moveNext()) {
        return false
      }
    }

    return true
  }
}

class FilterEnumerator<T> extends Enumerator<T> {
  private _filter: Predicate<T>

  constructor(enumerator: IEnumerator<T>, filter: Predicate<T>) {
    super(enumerator)
    this._filter = filter
  }

  moveNext(): boolean {
    let done = super.moveNext()

    while (!done && !this._filter(this.current, this._index)) {
      done = super.moveNext()
    }

    return done
  }
}

class FlatMapEnumerator<T, U = T> implements IEnumerator<U> {
  private _enumerator: IEnumerator<T>
  private _flatMap: Morphism<T, IEnumerator<U>>
  private _inner?: IEnumerator<U>
  private _index: number = -1

  constructor(enumerator: IEnumerator<T>, flatMap: Morphism<T, IEnumerator<U>>) {
    this._enumerator = enumerator
    this._flatMap = flatMap
  }

  get current(): Nullable<U> {
    return this._inner?.current
  }

  moveNext(): boolean {
    if (this._inner && this._inner.moveNext()) {
      return true
    }

    const done = this._enumerator.moveNext()

    if (done) {
      return false
    }

    this._index += 1
    this._inner = this._flatMap(this._enumerator.current, this._index)
    return this._inner.moveNext()
  }

  reset(): void {
    this._index = -1
    this._inner = undefined
    this._enumerator.reset()
  }

  toIterator<T>(): Iterator<T> {
    return Enumerator.toIterator(this) as Iterator<T>
  }
}

class MapEnumerator<T, U = T> implements IEnumerator<U> {
  private _enumerator: IEnumerator<T>
  private _map: Morphism<T, U>
  private _current!: U
  private _index: number = -1

  get current(): U {
    return this._current
  }

  constructor(enumerator: IEnumerator<T>, map: Morphism<T, U>) {
    this._enumerator = enumerator
    this._map = map
  }

  toIterator<U>(): Iterator<U> {
    return Enumerator.toIterator(this) as Iterator<U>
  }

  moveNext(): boolean {
    this._index += 1
    const done = this._enumerator.moveNext()

    if (!done) {
      this._current = this._map(this._enumerator.current, this._index)
    }

    return done
  }

  reset(): void {
    this._enumerator.reset()
  }
}

class TakeEnumerator<T> extends Enumerator<T> {
  private _limit: number

  constructor(enumerator: IEnumerator<T>, limit: number) {
    super(enumerator)
    this._limit = limit
  }

  moveNext(): boolean {
    if (this._limit < 0) {
      return false
    } else {
      this._limit -= 1
      return true
    }
  }
}

class FusedEnumerator<T> implements IEnumerator<T> {
  private _enumerators: IEnumerator<T>[]
  private _index: number = 0

  private _cur() {
    return this._enumerators[this._index]
  }

  private _done() {
    return this._index < this._enumerators.length
  }

  get current(): Nullable<T> {
    return this._cur().current
  }

  constructor(enumerators: IEnumerator<T>[]) {
    this._enumerators = enumerators
  }

  toIterator<T>(): Iterator<T> {
    return Enumerator.toIterator(this) as Iterator<T>
  }

  moveNext(): boolean {
    while (!this._done()) {
      if (this._cur().moveNext()) {
        return true
      }

      this._index += 1
    }
    return false
  }

  reset(): void {
    const len = this._enumerators.length

    for (let i = 0; i < len; i++) {
      this._enumerators[i].reset()
    }

    this._index = 0
  }
}

export class Enumerable<T> implements IEnumerable<T>, Iterable<T> {
  private _enumerator: IEnumerator<T>

  constructor(enumerator: IEnumerator<T>) {
    this._enumerator = enumerator
  }

  static from<T>(enumerator: IEnumerator<T>) {
    return new this(enumerator)
  }

  [Symbol.iterator](): Iterator<T> {
    return this._enumerator.toIterator()
  }

  at(index: number): Nullable<T> {
    for (let i = 0; i < index; i++) {
      if (!this._enumerator.moveNext()) {
        return
      }
    }

    const value = this._enumerator.current
    this._enumerator.reset()
    return value
  }

  atOrDefault(index: number, defaultValue: T) {
    const value = this.at(index)
    return value || defaultValue
  }

  atOrElse(index: number, defaultValue: () => T) {
    const value = this.at(index)
    return value || defaultValue()
  }

  concat(other: IEnumerator<T>): FusedEnumerator<T> {
    return new FusedEnumerator([this._enumerator, other])
  }


  drop(limit: number) {
    return new Enumerable(new DropEnumerator(this._enumerator, limit))
  }

  enumerator(): IEnumerator<T> {
    return this._enumerator
  }

  every(predicate: Predicate<T>): boolean {
    let index = 0
    while (this._enumerator.moveNext()) {
      if (!predicate(this._enumerator.current, index)) {
        return false
      }

      index += 1
    }

    this._enumerator.reset()
    return true
  }

  filter(predicate: Predicate<T>): IEnumerable<T> {
    return new Enumerable(new FilterEnumerator(this._enumerator, predicate))
  }

  flatMap<U>(fn: Morphism<T, IEnumerator<U>>): IEnumerable<U> {
    return new Enumerable(new FlatMapEnumerator(this._enumerator, fn))
  }

  map<U>(fn: Morphism<T, U>): IEnumerable<U> {
    return new Enumerable(new MapEnumerator(this._enumerator, fn))
  }

  some(predicate: Predicate<T>): boolean {
    let index = 0
    while (this._enumerator.moveNext()) {
      if (predicate(this._enumerator.current, index)) {
        return true
      }

      index += 1
    }

    this._enumerator.reset()
    return false
  }

  take(limit: number): IEnumerable<T> {
    return new Enumerable(new TakeEnumerator(this._enumerator, limit))
  }
}

