import { stringify } from '@preserves/core';
import * as M from './meta';

export function checkSchema(schema: M.Schema): (
    { ok: true, schema: M.Schema } | { ok: false, problems: Array<string> })
{
    const checker = new Checker();
    schema.definitions.forEach(checker.checkDefinition.bind(checker));
    if (checker.problems.length > 0) {
        return { ok: false, problems: checker.problems };
    } else {
        return { ok: true, schema };
    }
}

enum ValueAvailability {
    AVAILABLE,
    NOT_AVAILABLE,
};

class Checker {
    problems: Array<string> = [];

    recordProblem(context: string, detail: string): void {
        this.problems.push(`${detail} in ${context}`);
    }

    checkBinding(scope: Set<string>, sym: symbol, context: string): void {
        const name = sym.description!;
        if (scope.has(name)) {
            this.recordProblem(context, `duplicate binding named ${JSON.stringify(name)}`);
        }
        if (!M.isValidToken(name)) {
            this.recordProblem(context, `invalid binding name ${JSON.stringify(name)}`);
        }
        scope.add(name);
    }

    checkDefinition(def: M.Definition, name: symbol): void {
        switch (def._variant) {
            case 'or': {
                const labels = new Set<string>();
                [def.pattern0, def.pattern1, ... def.patternN].forEach(({ variantLabel, pattern }) => {
                    const context = `variant ${variantLabel} of ${name.description!}`;
                    if (labels.has(variantLabel)) {
                        this.recordProblem(context, `duplicate variant label`);
                    }
                    if (!M.isValidToken(variantLabel)) {
                        this.recordProblem(context, `invalid variant label`);
                    }
                    labels.add(variantLabel);
                    this.checkPattern(new Set(), pattern, context, ValueAvailability.AVAILABLE);
                });
                break;
            }
            case 'and': {
                const ps = [def.pattern0, def.pattern1, ... def.patternN];
                const scope = new Set<string>();
                ps.forEach((p) => this.checkNamedPattern(scope, p, name.description!));
                break;
            }
            case 'Pattern':
                this.checkPattern(
                    new Set(), def.value, name.description!, ValueAvailability.AVAILABLE);
                break;
        }
    }

    checkNamedPattern(scope: Set<string>, p: M.NamedPattern, context: string): void
    {
        switch (p._variant) {
            case 'named': {
                this.checkBinding(scope, p.value.name, context);
                this.checkPattern(scope,
                                  M.Pattern.SimplePattern(p.value.pattern),
                                  `${JSON.stringify(p.value.name.description!)} of ${context}`,
                                  ValueAvailability.AVAILABLE);
                break;
            }
            case 'anonymous':
                this.checkPattern(scope, p.value, context, ValueAvailability.NOT_AVAILABLE);
                break;
        }
    }

    checkPattern(scope: Set<string>,
                 p: M.Pattern,
                 context: string,
                 availability: ValueAvailability): void
    {
        switch (p._variant) {
            case 'SimplePattern':
                if (p.value._variant !== 'lit' && availability === ValueAvailability.NOT_AVAILABLE) {
                    this.recordProblem(context, 'cannot recover serialization of non-literal pattern');
                }
                if (p.value._variant === 'Ref' &&
                    !(M.isValidToken(p.value.value.name.description!) &&
                        p.value.value.module.every(n => M.isValidToken(n.description!))))
                {
                    this.recordProblem(context, 'invalid reference name');
                }
                break;
            case 'CompoundPattern':
                ((p: M.CompoundPattern): void => {
                    switch (p._variant) {
                        case 'rec':
                            this.checkNamedPattern(scope, p.label, `label of ${context}`);
                            this.checkNamedPattern(scope, p.fields, `fields of ${context}`);
                            break;
                        case 'tuple':
                            p.patterns.forEach((pp, i) =>
                                this.checkNamedPattern(scope, pp, `item ${i} of ${context}`));
                            break;
                        case 'tuplePrefix':
                            p.fixed.forEach((pp, i) =>
                                this.checkNamedPattern(scope, pp, `item ${i} of ${context}`));
                            this.checkNamedPattern(
                                scope, M.promoteNamedSimplePattern(p.variable), `tail of ${context}`);
                            break;
                        case 'dict':
                            p.entries.forEach((np, key) =>
                                this.checkNamedPattern(
                                    scope,
                                    M.promoteNamedSimplePattern(np),
                                    `entry ${stringify(key)} in dictionary in ${context}`));
                            break;
                    }
                })(p.value);
        }
    }
}
