import { FunctionContext } from "./context";
import * as M from '../meta';
import { Item, seq } from "./block";
import { simpleType, typeFor } from "./gentype";
import { ANY_TYPE, Type } from "./type";

export function converterForDefinition(
    ctx: FunctionContext,
    p: M.Definition,
    src: string,
    dest: string): Item[]
{
    switch (p._variant) {
        case 'or': {
            const alts = [p.pattern0, p.pattern1, ... p.patternN];
            function loop(i: number): Item[] {
                ctx.variantName = alts[i].variantLabel;
                return [... converterForPattern(ctx, alts[i].pattern, src, dest),
                        ... ((i < alts.length - 1)
                            ? [seq(`if (${dest} === void 0) `, ctx.block(() => loop(i + 1)))]
                            : [])];
            }
            return loop(0);
        }
        case 'and': {
            const pcs = [p.pattern0, p.pattern1, ... p.patternN];
            function loop(i: number): Item[] {
                return (i < pcs.length)
                    ? converterFor(ctx, pcs[i], src, () => loop(i + 1))
                    : [ctx.buildCapturedCompound(dest)];
            }
            return loop(0);
        }
        case 'Pattern':
            ctx.variantName = void 0;
            return converterForPattern(ctx, p.value, src, dest);
    }
}

function converterForPattern(
    ctx: FunctionContext,
    p: M.Pattern,
    src: string,
    dest: string): Item[]
{
    return converterFor(ctx, M.NamedPattern.anonymous(p), src, simpleValue => {
        if (simpleValue === void 0) {
            return [ctx.buildCapturedCompound(dest)];
        } else if (ctx.variantName !== void 0) {
            if (typeFor(ctx.mod.resolver(), p).kind === 'unit') {
                return [ctx.buildCapturedCompound(dest)];
            } else {
                return [ctx.withCapture('value',
                                        simpleValue,
                                        () => ctx.buildCapturedCompound(dest))];
            }
        } else {
            if (typeFor(ctx.mod.resolver(), p).kind === 'unit') {
                return [ctx.buildCapturedCompound(dest)];
            } else {
                return [`${dest} = ${simpleValue}`];
            }
        }
    });
}

function converterForTuple(ctx: FunctionContext,
                           ps: M.NamedPattern[],
                           src: string,
                           knownArray: boolean,
                           variablePattern: M.NamedSimplePattern | undefined,
                           k: () => Item[]): Item[]
{
    function loop(i: number): Item[] {
        if (i < ps.length) {
            return converterFor(ctx, ps[i], `${src}[${i}]`, () => loop(i + 1));
        } else {
            if (variablePattern === void 0) {
                return k();
            } else {
                const vN = ctx.gentemp(Type.array(ANY_TYPE));
                return [ps.length > 0 ? `${vN} = ${src}.slice(${ps.length})` : `${vN} = ${src}`,
                        ... converterFor(ctx, M.promoteNamedSimplePattern(variablePattern), vN, k, true)];
            }
        }
    }

    const lengthCheck = variablePattern === void 0
        ? seq(` && ${src}.length === ${ps.length}`)
        : ((ps.length === 0) ? '' : seq(` && ${src}.length >= ${ps.length}`));

    return knownArray
        ? loop(0)
        : [seq(`if (_.isSequence(${src})`, lengthCheck, `) `, ctx.block(() => loop(0)))];
}

function converterFor(
    ctx: FunctionContext,
    np: M.NamedPattern,
    src: string,
    ks: (dest: string | undefined) => Item[],
    knownArray = false): Item[]
{
    let p = M.unnamePattern(np);
    let maybeName = M.nameFor(np);

    if (p._variant === 'SimplePattern') {
        const dest = ctx.gentemp(simpleType(ctx.mod.resolver(), p.value));
        return [... converterForSimple(ctx, p.value, src, dest, knownArray),
                ctx.convertCapture(maybeName, dest, () => ks(dest))];
    } else {
        return converterForCompound(ctx, p.value, src, knownArray, () => ks(void 0));
    }
}

export function converterForSimple(
    ctx: FunctionContext,
    p: M.SimplePattern,
    src: string,
    dest: string,
    knownArray: boolean): Item[]
{
    switch (p._variant) {
        case 'any':
            return [`${dest} = ${src}`];
        case 'atom': {
            let test: Item;
            let valexp: Item = `${src}`;
            switch (p.atomKind._variant) {
                case 'Boolean': test = `typeof ${src} === 'boolean'`; break;
                case 'Float': test = `_.Float.isSingle(${src})`; valexp = `${src}.value`; break;
                case 'Double': test =`_.Float.isDouble(${src})`; valexp = `${src}.value`; break;
                case 'SignedInteger': test = `typeof ${src} === 'number'`; break;
                case 'String': test = `typeof ${src} === 'string'`; break;
                case 'ByteString': test = `_.Bytes.isBytes(${src})`; break;
                case 'Symbol': test = `typeof ${src} === 'symbol'`; break;
            }
            return [seq(`${dest} = `, test, ` ? `, valexp, ` : void 0`)];
        }
        case 'embedded':
            return [`${dest} = _.isEmbedded<_embedded>(${src}) ? ${src}.embeddedValue : void 0`];
        case 'lit':
            return [`${dest} = _.is(${src}, ${ctx.mod.literal(p.value)}) ? {} : void 0`];

        case 'seqof': {
            const kKnownArray = () => {
                const v = ctx.gentempname();
                return [
                    seq(`${dest} = []`),
                    seq(`for (const ${v} of ${src}) `, ctx.block(() => [
                        ... converterFor(ctx, M.anonymousSimplePattern(p.pattern), v, vv =>
                            [`${dest}.push(${vv})`, `continue`]),
                        seq(`${dest} = void 0`),
                        seq(`break`)]))];
            };
            if (knownArray) {
                return kKnownArray();
            } else {
                return [`${dest} = void 0`,
                        seq(`if (_.isSequence(${src})) `, ctx.block(kKnownArray))];
            }
        }
        case 'setof':
            return [`${dest} = void 0`,
                    seq(`if (_.Set.isSet<_embedded>(${src})) `, ctx.block(() => {
                        const v = ctx.gentempname();
                        return [
                            seq(`${dest} = new _.KeyedSet()`),
                            seq(`for (const ${v} of ${src}) `, ctx.block(() => [
                                ... converterFor(ctx, M.anonymousSimplePattern(p.pattern), v, vv =>
                                    [`${dest}.add(${vv})`, `continue`]),
                                seq(`${dest} = void 0`),
                                seq(`break`)]))];
                    }))];
        case 'dictof':
            return [`${dest} = void 0`,
                    seq(`if (_.Dictionary.isDictionary<_embedded>(${src})) `, ctx.block(() => {
                        const v = ctx.gentempname();
                        const k = ctx.gentempname();
                        return [
                            seq(`${dest} = new _.KeyedDictionary()`),
                            seq(`for (const [${k}, ${v}] of ${src}) `, ctx.block(() => [
                                ... converterFor(ctx, M.anonymousSimplePattern(p.key), k, kk =>
                                    converterFor(ctx, M.anonymousSimplePattern(p.value), v, vv =>
                                        [`${dest}.set(${kk}, ${vv})`, `continue`])),
                                seq(`${dest} = void 0`),
                                seq(`break`)]))];
                    }))];
        case 'Ref':
            return ctx.mod.lookup(
                p.value,
                (_p, _t) => [`${dest} = to${p.value.name.description!}(${src})`],
                (modPath, modId, modFile, modExpr, _p, _t) => {
                    ctx.mod.imports.add([modPath, modId, modFile, modExpr]);
                    return [`${dest} = ${modId}${modExpr}.to${p.value.name.description!}${ctx.mod.genericArgs()}(${src})`];
                });
        default:
            ((_p: never) => {})(p);
            throw new Error("Unreachable");
    }
}

function converterForCompound(
    ctx: FunctionContext,
    p: M.CompoundPattern,
    src: string,
    knownArray: boolean,
    ks: () => Item[]): Item[]
{
    switch (p._variant) {
        case 'rec':
            return [seq(`if (_.Record.isRecord<_.Value<_embedded>, _.Tuple<_.Value<_embedded>>, _embedded>(${src})) `, ctx.block(() =>
                converterFor(ctx, p.label, `${src}.label`, () =>
                    converterFor(ctx, p.fields, src, ks, true))))];
        case 'tuple':
            return converterForTuple(ctx, p.patterns, src, knownArray, void 0, ks);
        case 'tuplePrefix':
            return converterForTuple(ctx, p.fixed, src, knownArray, p.variable, ks);
        case 'dict': {
            const entries = Array.from(p.entries);
            function loop(i: number): Item[] {
                if (i < entries.length) {
                    const [k, n] = entries[i];
                    const tmpSrc = ctx.gentemp();
                    return [seq(`if ((${tmpSrc} = ${src}.get(${ctx.mod.literal(k)})) !== void 0) `,
                                ctx.block(() =>
                                    converterFor(
                                        ctx,
                                        M.promoteNamedSimplePattern(n),
                                        tmpSrc,
                                        () => loop(i + 1))))];
                } else {
                    return ks();
                }
            }
            return [seq(`if (_.Dictionary.isDictionary<_embedded>(${src})) `, ctx.block(() => loop(0)))];
        }
        default:
            ((_p: never) => {})(p);
            throw new Error("Unreachable");
    }
}
