import { FormatScanner, TokeType } from "./FormatScanner";
import { isNullOrEmptyString, isPrimitive, get } from "../safe";
import { TextWriter, MapOf } from "../interfaces";

type CompiledPattern = (writer: TextWriter, args: any) => void;

export class FormatCompiler {
    _scanner: FormatScanner;
    static _cache: MapOf<CompiledPattern> = {};

    _parts: Array<string | { name: string; format?: string; }>;

    constructor(scanner: FormatScanner) {
        this._scanner = scanner;
        this._parts = [];
    }

    compile() {
        this.visitText();
        const parts = this._parts;

        return (writer: TextWriter, args: any) => {
            parts.forEach(x => {
                if (isPrimitive(x))
                    writer.writeValue(x);
                else
                    writer.writeValue(get(x.name, args), x.format);
            });
        };
    }

    static compile(pattern: string) {
        let compiledPattern = this._cache && this._cache[pattern];
        if (!compiledPattern) {
            const compiler = new this(new FormatScanner(pattern));

            compiledPattern = compiler.compile();

            this._cache[pattern] = compiledPattern;
        }
        return compiledPattern;
    }

    visitText() {
        while (this._scanner.next()) {
            // console.log(this._scanner.getTokenType(), this._scanner.getTokenValue());
            switch (this._scanner.getTokenType()) {
                case TokeType.CurlOpen:
                    this.visitCurlOpen();
                    break;
                case TokeType.CurlClose:
                    this.visitCurlClose();
                    break;
                default:
                    this.pushText(this._scanner.getTokenValue());
            }
        }
    }

    visitCurlClose() {
        if (!this._scanner.next())
            this.dieUnexpectedEnd("}");
        if (this._scanner.getTokenType() !== TokeType.CurlClose)
            this.dieUnexpectedToken("}");
        this.pushText("}");
    }

    visitCurlOpen() {
        if (!this._scanner.next())
            this.dieUnexpectedEnd("{ | TEXT");

        if (this._scanner.getTokenType() === TokeType.CurlOpen)
            this.pushText("{");
        else
            this.visitTemplateSubst();

    }

    visitTemplateSubst() {
        if (this._scanner.getTokenType() !== TokeType.Text)
            this.dieUnexpectedToken("TEXT");

        const fieldName = this._scanner.getTokenValue();
        const filedFormat = this.readColon() ? this.readFieldFormat() : undefined;

        if (this._scanner.getTokenType() !== TokeType.CurlClose)
            this.dieUnexpectedToken("}");

        this.pushSubst(fieldName, filedFormat);
    }

    readFieldFormat() {
        const parts = new Array<string>();
        do {
            if (this._scanner.getTokenType() === TokeType.CurlClose) {
                return parts.join("");
            } else {
                parts.push(this._scanner.getTokenValue());
            }
        } while (this._scanner.next());

        this.dieUnexpectedEnd("}");
    }

    readColon() {
        if (!this._scanner.next())
            this.dieUnexpectedEnd();
        if (this._scanner.getTokenType() !== TokeType.Colon)
            return false;
        if (!this._scanner.next())
            this.dieUnexpectedEnd();
        return true;
    }

    pushSubst(fieldName: string, filedFormat?: string) {
        // console.log("pushSubst ", fieldName, filedFormat);
        this._parts.push({ name: fieldName, format: filedFormat });
    }

    pushText(text: string) {
        this._parts.push(text);
    }

    dieUnexpectedToken(expected?: string): never {
        throw new Error(isNullOrEmptyString(expected) ?
            `Unexpected token ${this._scanner.getTokenValue()}` :
            `Unexpected token ${this._scanner.getTokenValue()}, expected ${expected}`
        );
    }

    dieUnexpectedEnd(expected?: string): never {
        throw new Error(isNullOrEmptyString(expected) ? "Unexpected end of the string" : `Unexpected end of the string, expected ${expected}`);
    }
}
