import { Reader, Annotated, Dictionary, is, peel, preserves, Record, strip, Tuple, Position, position, stringify, isCompound, KeyedDictionary, annotate, annotations, isEmbedded, GenericEmbedded, genericEmbeddedTypeDecode } from '@preserves/core';
import { Input, Pattern, Schema, Definition, CompoundPattern, SimplePattern } from './meta';
import * as M from './meta';
import { SchemaSyntaxError } from './error';
import { checkSchema } from './checker';

const positionTable = new WeakMap<object, Position>();

export function recordPosition<X extends object>(v: X, pos: Position | null): X {
    if (pos === null) { console.error('Internal error in Schema reader: null source position for', v); }
    if (pos !== null) positionTable.set(v, pos);
    return v;
}

export function refPosition(v: object): Position | null {
    return positionTable.get(v) ?? null;
}

function splitBy<T>(items: Array<T>, separator: T): Array<Array<T>> {
    const groups: Array<Array<T>> = [];
    let group: Array<T> = [];
    function finish() {
        if (group.length > 0) {
            groups.push(group);
            group = [];
        }
    }
    for (const item of items) {
        if (is(item, separator)) {
            finish();
        } else {
            group.push(item);
        }
    }
    finish();
    return groups;
}

function invalidClause(clause: Array<Input>): never {
    throw new SchemaSyntaxError(preserves`Invalid Schema clause: ${clause}`,
                                position(clause[0] ?? false));
}

function invalidPattern(name: string, item: Input, pos: Position | null): never {
    throw new SchemaSyntaxError(`Invalid pattern in ${name}: ${stringify(item)}`, pos);
}

export type SchemaReaderOptions = {
    name?: string | Position;
    readInclude?(includePath: string): string;
};

function _readSchema(source: string, options?: SchemaReaderOptions): Array<Input> {
    return new Reader<GenericEmbedded>(source, {
        name: options?.name,
        includeAnnotations: true,
        embeddedDecode: genericEmbeddedTypeDecode,
    }).readToEnd();
}

export function readSchema(source: string, options?: SchemaReaderOptions): Schema
{
    const checked = checkSchema(parseSchema(_readSchema(source, options), options ?? {}));
    if (checked.ok) return checked.schema;
    throw new Error(`Schema is not invertible:\n` +
        checked.problems.map(c => ' - ' + c).join('\n'));
}

export function parseSchema(toplevelTokens: Array<Input>, options: SchemaReaderOptions): Schema
{
    let version: M.Version | undefined = void 0;
    let embeddedType: M.EmbeddedTypeName = M.EmbeddedTypeName.$false();
    let definitions = new KeyedDictionary<symbol, Definition, M.InputEmbedded>();

    function process(toplevelTokens: Array<Input>): void {
        const toplevelClauses = splitBy(peel(toplevelTokens) as Array<Input>, M.DOT);
        for (const clause of toplevelClauses) {
            if (clause.length >= 2 && is(clause[1], M.EQUALS)) {
                const pos = position(clause[0]);
                const name = peel(clause[0]);
                if (typeof name !== 'symbol') invalidClause(clause);
                if (!M.isValidToken(name.description!)) {
                    throw new SchemaSyntaxError(preserves`Invalid definition name: ${name}`, pos);
                }
                if (definitions.has(name)) {
                    throw new SchemaSyntaxError(preserves`Duplicate definition: ${clause}`, pos);
                }
                definitions.set(name, parseDefinition(name, pos, clause.slice(2)));
            } else if (clause.length === 2 && is(clause[0], M.$version)) {
                version = M.asVersion(peel(clause[1]));
            } else if (clause.length === 2 && is(clause[0], M.$embeddedType)) {
                const pos = position(clause[1]);
                const stx = peel(clause[1]);
                if (stx === false) {
                    embeddedType = M.EmbeddedTypeName.$false();
                } else if (typeof stx === 'symbol' && M.isValidQid(stx.description!)) {
                    embeddedType = M.EmbeddedTypeName.Ref(parseRef(stx.description!, pos));
                } else {
                    invalidPattern('embedded type name specification', stx, pos);
                }
            } else if (clause.length === 2 && is(clause[0], M.INCLUDE)) {
                const pos = position(clause[1]);
                const path = peel(clause[1]);
                if (typeof path !== 'string') {
                    throw new SchemaSyntaxError(preserves`Invalid include: ${clause}`, pos);
                }
                if (options.readInclude === void 0) {
                    throw new SchemaSyntaxError(preserves`Cannot include files in schema`, pos);
                }
                process(_readSchema(options.readInclude(path), options));
            } else {
                invalidClause(clause);
            }
        }
    }

    process(toplevelTokens);

    if (version === void 0) {
        throw new SchemaSyntaxError("Schema: missing version declaration.", null);
    }

    return M.Schema({ version: M.Version(), embeddedType, definitions });
}

function namedMustBeSimple(p: Position | null): never {
    throw new SchemaSyntaxError('Named patterns must be Simple patterns', p);
}

function parseDefinition(name: symbol, pos: Position | null, body: Array<Input>): Definition {
    function alternativeName(input: Array<Input>): M.NamedAlternative
    {
        const n = findName(input) || findName(input[0]);
        const p = parsePattern(name, input);
        if (n !== false) {
            return M.NamedAlternative({ variantLabel: n.description!, pattern: p });
        }
        if (p._variant === 'CompoundPattern' &&
            p.value._variant === 'rec' &&
            p.value.label._variant === 'anonymous' &&
            p.value.label.value._variant === 'SimplePattern' &&
            p.value.label.value.value._variant === 'lit' &&
            typeof p.value.label.value.value.value === 'symbol' &&
            M.isValidToken(p.value.label.value.value.value.description!))
        {
            return M.NamedAlternative({
                variantLabel: p.value.label.value.value.value.description!,
                pattern: p
            });
        }
        if (p._variant === 'SimplePattern' &&
            p.value._variant === 'Ref' &&
            M.isValidQid(p.value.value.name.description!))
        {
            return M.NamedAlternative({
                variantLabel: M.qidLast(p.value.value.name.description!),
                pattern: p
            });
        }
        if (p._variant === 'SimplePattern' &&
            p.value._variant === 'lit')
        {
            const s = M.namelike(p.value.value);
            if (s !== void 0) return M.NamedAlternative({ variantLabel: s, pattern: p });
        }
        throw new SchemaSyntaxError(preserves`Name missing for alternative: ${input}`, pos);
    }

    function patternName(input: Array<Input>): M.NamedPattern {
        const n = findName(input) || findName(input[0]);
        const p = parsePattern(name, input);
        if (n !== false) {
            if (p._variant !== 'SimplePattern') namedMustBeSimple(position(input[0]));
            return M.NamedPattern.named(M.Binding({ name: n, pattern: p.value }));
        }
        return M.NamedPattern.anonymous(p);
    }

    const andPieces = splitBy(body, M.ANDSYM);
    const orPieces = splitBy(body, M.ORSYM);

    if (andPieces.length === 0 || orPieces.length === 0) {
        throw new SchemaSyntaxError(preserves`Invalid Schema clause: ${body}`, pos);
    }

    if (andPieces.length > 1 && orPieces.length > 1) {
        throw new SchemaSyntaxError(preserves`Mixed "or" and "and" clause: ${body}`, pos);
    }

    if (andPieces.length > 1) {
        return M.Definition.and({
            pattern0: patternName(andPieces[0]),
            pattern1: patternName(andPieces[1]),
            patternN: andPieces.slice(2).map(patternName),
        });
    }

    if (orPieces.length > 1) {
        return M.Definition.or({
            pattern0: alternativeName(orPieces[0]),
            pattern1: alternativeName(orPieces[1]),
            patternN: orPieces.slice(2).map(alternativeName),
        });
    }

    return M.Definition.Pattern(parsePattern(name, orPieces[0]));
}

function transferAnnotations(dest: Input, src: Input): Input {
    return annotate(dest, ... annotations(src));
}

function parsePattern(name: symbol, body0: Array<Input>): Pattern {
    function parseSimple<A>(item0: Input, ks: (p: SimplePattern) => A, kf: () => A): A {
        const pos = position(item0);
        const item = peel(item0);
        function complain(): never { invalidPattern(stringify(name), item, pos); }
        if (typeof item === 'symbol') {
            const str = item.description!;
            switch (str) {
                case 'any': return ks(M.SimplePattern.any());
                case 'bool': return ks(M.SimplePattern.atom(M.AtomKind.Boolean()));
                case 'float': return ks(M.SimplePattern.atom(M.AtomKind.Float()));
                case 'double': return ks(M.SimplePattern.atom(M.AtomKind.Double()));
                case 'int': return ks(M.SimplePattern.atom(M.AtomKind.SignedInteger()));
                case 'string': return ks(M.SimplePattern.atom(M.AtomKind.String()));
                case 'bytes': return ks(M.SimplePattern.atom(M.AtomKind.ByteString()));
                case 'symbol': return ks(M.SimplePattern.atom(M.AtomKind.Symbol()));
                default: {
                    if (str[0] === '=') {
                        return ks(M.SimplePattern.lit(Symbol.for(str.slice(1))));
                    } else if (M.isValidQid(str)) {
                        return ks(M.SimplePattern.Ref(parseRef(str, pos)));
                    } else {
                        complain();
                    }
                }
            }
        } else if (Record.isRecord<Input, Tuple<Input>, M.InputEmbedded>(item)) {
            const label = item.label;
            if (Record.isRecord<Input, [], M.InputEmbedded>(label)) {
                if (label.length !== 0) complain();
                switch (label.label) {
                    case M.$lit:
                        if (item.length !== 1) complain();
                        return ks(M.SimplePattern.lit(item[0]));
                    default:
                        return kf();
                }
            } else {
                return kf();
            }
        } else if (Array.isArray(item) && item.length === 2 && is(item[1], M.DOTDOTDOT)) {
            return ks(M.SimplePattern.seqof(walkSimple(item[0])));
        } else if (Set.isSet<M.Input>(item)) {
            if (item.size !== 1) complain();
            const [vp] = item.values();
            return ks(M.SimplePattern.setof(walkSimple(vp)));
        } else if (Dictionary.isDictionary<M.InputEmbedded, Input>(item)
            && item.size === 2
            && item.has(M.DOTDOTDOT))
        {
            const v = item.clone();
            v.delete(M.DOTDOTDOT);
            const [[kp, vp]] = v.entries();
            return ks(M.SimplePattern.dictof({ key: walkSimple(kp), value: walkSimple(vp) }));
        } else if (isCompound(item)) {
            return kf();
        } else if (isEmbedded(item)) {
            return ks(M.SimplePattern.embedded(walkSimple(item.embeddedValue.generic)));
        } else {
            return ks(M.SimplePattern.lit(strip(item)));
        }
    }

    function parseCompound(item0: Input): CompoundPattern {
        const pos = position(item0);
        const item = peel(item0);
        function complain(): never { invalidPattern(stringify(name), item, pos); }

        if (Record.isRecord<Input, Tuple<Input>, M.InputEmbedded>(item)) {
            const label = item.label;
            if (Record.isRecord<Input, [], M.InputEmbedded>(label)) {
                if (label.length !== 0) complain();
                switch (label.label) {
                    case M.$rec:
                        if (item.length !== 2) complain();
                        return M.CompoundPattern.rec({
                            label: maybeNamed(item[0]),
                            fields: maybeNamed(item[1])
                        });
                    default:
                        complain();
                }
            } else {
                return M.CompoundPattern.rec({
                    label: M.NamedPattern.anonymous(M.Pattern.SimplePattern(M.SimplePattern.lit(label))),
                    fields: M.NamedPattern.anonymous(parsePattern(name, [transferAnnotations([... item], item0)])),
                });
            }
        } else if (Array.isArray(item) && item.length > 2 && is(item[item.length - 1], M.DOTDOTDOT)) {
            const variableTemplateInput = item[item.length - 2];
            const variablePart =
                transferAnnotations([variableTemplateInput, M.DOTDOTDOT], variableTemplateInput);
            return M.CompoundPattern.tuplePrefix({
                fixed: item.slice(0, item.length - 2).map(maybeNamed),
                variable: maybeNamedSimple(variablePart),
            });
        } else if (Array.isArray(item)) {
            return M.CompoundPattern.tuple(item.map(maybeNamed));
        } else if (Dictionary.isDictionary<M.InputEmbedded, Input>(item) && !item.has(M.DOTDOTDOT)) {
            return M.CompoundPattern.dict(
                M.DictionaryEntries(item.mapEntries<M.NamedSimplePattern, Input, M.InputEmbedded>(
                    ([k, vp]) => [
                        strip(k),
                        _maybeNamed(
                            M.NamedSimplePattern.named,
                            M.NamedSimplePattern.anonymous,
                            walkSimple,
                            strip(k))(vp)
                    ])));
        } else {
            complain();
        }
    }

    const walk = (b: Input): Pattern => parsePattern(name, [b]);
    const walkSimple = (b: Input): SimplePattern => parseSimple(b, p => p, () => {
        throw new SchemaSyntaxError(`Compound patterns not accepted here`, position(b));
    });

    function _maybeNamed<R,P>(
        named: (p: M.Binding) => R,
        anonymous: (p: P) => R,
        recur: (b: Input) => P,
        literalName?: Input): (b: Input) => R
    {
        return (b: Input) => {
            let name = findName(b);
            if (name === false) {
                if (literalName !== void 0 &&
                    typeof literalName === 'symbol' &&
                    M.isValidToken(literalName.description!))
                {
                    name = literalName;
                }
            }
            if (name === false) {
                return anonymous(recur(b));
            }
            return named(M.Binding({ name, pattern: parseSimple(b, p => p, () =>
                namedMustBeSimple(position(b))) }));
        };
    }
    const maybeNamed = _maybeNamed(M.NamedPattern.named, M.NamedPattern.anonymous, walk);
    const maybeNamedSimple =
        _maybeNamed(M.NamedSimplePattern.named, M.NamedSimplePattern.anonymous, walkSimple);

    const body = peel(body0) as Array<Input>;
    if (body.length !== 1) {
        invalidPattern(stringify(name), body, body.length > 0 ? position(body[0]) : position(body));
    }
    return parseSimple(body[0],
                       M.Pattern.SimplePattern,
                       () => M.Pattern.CompoundPattern(parseCompound(body[0])));
}

function findName(x: Input): symbol | false {
    if (!Annotated.isAnnotated(x)) return false;
    for (const a0 of x.annotations) {
        const a = peel(a0);
        if (typeof a === 'symbol') return M.isValidToken(a.description!) && a;
    }
    return false;
}

function parseRef(s: string, pos: Position | null): M.Ref {
    const pieces = s.split('.');
    return recordPosition(M.Ref({
        module: M.ModulePath(pieces.slice(0, pieces.length - 1).map(Symbol.for)),
        name: Symbol.for(pieces[pieces.length - 1])
    }), pos);
}
