import { Record, Tuple } from "./record";
import { Bytes } from "./bytes";
import { fold } from "./fold";
import { is } from "./is";
import { Value } from "./values";
import { Set, Dictionary } from "./dictionary";
import { Annotated } from "./annotated";
import { unannotate } from "./strip";
import { embed, isEmbedded, Embedded } from "./embedded";

export function merge<T>(
    mergeEmbeddeds: (a: T, b: T) => T | undefined,
    item0: Value<T>,
    ... items: Array<Value<T>>): Value<T>
{
    function die(): never {
        throw new Error("Cannot merge items");
    }

    function walk(a: Value<T>, b: Value<T>): Value<T> {
        if (a === b) return a;
        return fold<T, Value<T>>(a, {
            boolean: die,
            single(_f: number) { return is(a, b) ? a : die(); },
            double(_f: number) { return is(a, b) ? a : die(); },
            integer: die,
            string: die,
            bytes(_b: Bytes) { return is(a, b) ? a : die(); },
            symbol: die,

            record(r: Record<Value<T>, Tuple<Value<T>>, T>) {
                if (!Record.isRecord<Value<T>, Tuple<Value<T>>, T>(b)) die();
                return Record(walk(r.label, b.label), walkMany(r, b));
            },
            array(a: Array<Value<T>>) {
                if (!Array.isArray(b) || Record.isRecord(b)) die();
                return walkMany(a, b);
            },
            set(_s: Set<T>) { die(); },
            dictionary(d: Dictionary<T>) {
                if (!Dictionary.isDictionary<T>(b)) die();
                const r = new Dictionary<T>();
                d.forEach((av,ak) => {
                    const bv = b.get(ak);
                    r.set(ak, bv === void 0 ? av : walk(av, bv));
                });
                b.forEach((bv, bk) => {
                    if (!d.has(bk)) r.set(bk, bv);
                });
                return r;
            },

            annotated(a: Annotated<T>) {
                return walk(a, unannotate(b));
            },

            embedded(t: Embedded<T>) {
                if (!isEmbedded<T>(b)) die();
                const r = mergeEmbeddeds(t.embeddedValue, b.embeddedValue);
                if (r === void 0) die();
                return embed(r);
            },
        });
    }

    function walkMany(a: Array<Value<T>>, b: Array<Value<T>>): Array<Value<T>> {
        if (a.length <= b.length) {
            return b.map((bb, i) => (i < a.length) ? walk(a[i], bb) : bb);
        } else {
            return a.map((aa, i) => (i < b.length) ? walk(aa, b[i]) : aa);
        }
    }

    return items.reduce(walk, item0);
}
