export type Item = Emittable | string;

export const DEFAULT_WIDTH = 80;

export class Formatter {
    width = DEFAULT_WIDTH;
    indentDelta = '    ';
    currentIndent = '\n';
    buffer: Array<string> = [];

    get indentSize(): number { return this.indentDelta.length; }
    set indentSize(n: number) { this.indentDelta = new Array(n + 1).join(' '); }

    write(i: Item) {
        if (typeof i === 'string') {
            this.buffer.push(i);
        } else {
            i.writeOn(this);
        }
    }

    newline() {
        this.write(this.currentIndent);
    }

    toString(): string {
        return this.buffer.join('');
    }

    withIndent(f: () => void): void {
        const oldIndent = this.currentIndent;
        try {
            this.currentIndent = this.currentIndent + this.indentDelta;
            f();
        } finally {
            this.currentIndent = oldIndent;
        }
    }

    clone(): Formatter {
        const f = Object.assign(new Formatter(), this);
        f.buffer = [];
        return f;
    }
}

export function formatItems(i: Item[], width = DEFAULT_WIDTH): string {
    const f = new Formatter();
    f.width = width;
    i.forEach(i => f.write(i));
    return f.toString();
}

export interface Emittable {
    writeOn(f: Formatter): void;
}

export class Sequence implements Emittable {
    items: Array<Item>;

    constructor(items: Array<Item>) {
        if (items.some(i => i === void 0)) throw new Error('aiee');
        this.items = items;
    }

    get separator(): string { return ''; }
    get terminator(): string { return ''; }

    writeOn(f: Formatter): void {
        let needSeparator = false;
        this.items.forEach(i => {
            if (needSeparator) {
                f.write(this.separator);
            } else {
                needSeparator = true;
            }
            f.write(i);
        });
        f.write(this.terminator);
    }
}

export class CommaSequence extends Sequence {
    get separator(): string { return ', '; }
}

export abstract class Grouping extends CommaSequence {
    abstract get open(): string;
    abstract get close(): string;

    writeHorizontally(f: Formatter): void {
        f.write(this.open);
        super.writeOn(f);
        f.write(this.close);
    }

    writeVertically(f: Formatter): void {
        f.write(this.open);
        if (this.items.length > 0) {
            f.withIndent(() => {
                this.items.forEach((i, index) => {
                    f.newline();
                    f.write(i);
                    const delim = index === this.items.length - 1 ? this.terminator : this.separator;
                    f.write(delim.trimRight());
                });
            });
            f.newline();
        }
        f.write(this.close);
    }

    writeOn(f: Formatter): void {
        const g = f.clone();
        this.writeHorizontally(g);
        const s = g.toString();
        if (s.length <= f.width) {
            f.write(s);
        } else {
            this.writeVertically(f);
        }
    }
}

export class Parens extends Grouping {
    get open(): string { return '('; }
    get close(): string { return ')'; }
}

export class OperatorSequence extends Parens {
    operator: string;

    constructor(operator: string, items: Array<Item>) {
        super(items);
        this.operator = operator;
    }

    get separator(): string { return this.operator; }
}

export class Brackets extends Grouping {
    get open(): string { return '['; }
    get close(): string { return ']'; }
}

export class AngleBrackets extends Grouping {
    get open(): string { return '<'; }
    get close(): string { return '>'; }
}

export class Braces extends Grouping {
    get open(): string { return '{'; }
    get close(): string { return '}'; }
}

export class Block extends Braces {
    get separator(): string { return '; ' }
    get terminator(): string { return ';' }
}

export const seq = (... items: Item[]) => new Sequence(items);
export const commas = (... items: Item[]) => new CommaSequence(items);
export const parens = (... items: Item[]) => new Parens(items);
export const opseq = (zero: string, op: string, ... items: Item[]) =>
    (items.length === 0) ? zero : new OperatorSequence(op, items);
export const brackets = (... items: Item[]) => new Brackets(items);
export const anglebrackets = (... items: Item[]) => new AngleBrackets(items);
export const braces = (... items: Item[]) => new Braces(items);
export const block = (... items: Item[]) => {
    if (items.length === 1 && items[0] instanceof Block) {
        return items[0];
    } else {
        return new Block(items);
    }
}
export const fnblock = (... items: Item[]) => seq('((() => ', block(... items), ')())');
export const keyvalue = (k: string, v: Item) => seq(JSON.stringify(k), ': ', v);
