/** return whether arg is T or an iterable of T
 * for the flat function */
function isIterable<T>(arg: T | Iterable<T>): arg is Iterable<T> {
    return typeof arg === "object" && Symbol.iterator in arg;
}

/** iterable wrapper for functional programming with lazy composition */
export default class Lazy<T> implements Iterator<T> {

    static from<T>(iterable: Iterable<T>) {
        return new Lazy<T>(iterable[Symbol.iterator]())
    }

    public constructor(protected iterator: Iterator<T>) {}

    public next(): ReturnType<Iterator<T>["next"]> {
        return this.iterator.next()
    }

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

    public filter(predicate: (t: T) => boolean) {
        const _this = this
        return Lazy.from({
            *[Symbol.iterator]() {
                for (const t of _this)
                    if (predicate(t))
                        yield t
            }
        })
    }

    public map<U>(transform: (t: T) => U) {
        const _this = this
        return Lazy.from<U>({
            *[Symbol.iterator]() {
                for (const t of _this)
                    yield transform(t)
            }
        })
    }

    public flat(depth = 1) {
        const _this = this;
        if (depth <= 0) return this;
        else return Lazy.from({
            *[Symbol.iterator]() {
                for (const item of _this) {
                    if (isIterable(item))
                        yield* Lazy.from(item).flat(depth - 1)
                    else yield item
                }
            }
        })
    }

    public concat(...args: (Iterable<T> | T)[] ): Lazy<T> {
        const _this = this
        return Lazy.from({
            *[Symbol.iterator]() {
                yield* _this;
                for (const arg of args)
                    if (isIterable(arg)) yield* arg;
                    else yield arg;
            }
        })
    }

    public forEach(doSomething: (t: T) => void) {
        for (const item of this)
            doSomething(item)
    }

    public take(n: number): Lazy<T> {
        const _this = this;
        return Lazy.from({
            *[Symbol.iterator]() {
                let i = 0;
                for (const item of _this) {
                    if (!(i < n)) break
                    yield item
                    i++
                }
            }
        })
    }

    public reduce<Result>(callback: (prev: Result, curr: T, index: number) => Result, initial: Result): Result {
        let result = initial;
        let i = 0;
        for (const curr of this) {
            result = callback(result, curr, i);
            i++;
        }
        return result;
    }

    public toSet(): Set<T> {
        const result = new Set<T>()
        for (const item of this) 
            result.add(item)
        return result
    }

    public some(predicate: (t: T) => boolean): boolean {
        for (const item of this)
            if (predicate(item)) return true
        return false
    }

    public every(predicate: (t: T) => boolean): boolean {
        return !this.some(t => !predicate(t))
    }
    
    public empty(): boolean {
        const item = this.next()
        return item.done
    }

    public sort(...[sortFunc]: Parameters<Array<T>["sort"]>) {
        return Lazy.from([...this].sort(sortFunc))
    }

    public get length() {
        let i = 0
        for (const item of this) i++
        return i
    }

    public includes(t: T) {
        for (const item of this) if (item === t) return true;
        return false;
    }

    public find(predicate: (t: T) => boolean) {
        for (const item of this) if (predicate(item)) return item;
        return false;
    }
}

/**

Allows stuff like following:

[...Lazy.from([1,2,3,[4,[5]]])
    .concat([1,2,3])
    .flat(2)
    .filter(x => x%2==0)
    .map(x => x * 3)
]

*/