import { Record, Tuple } from "./record";
import { Bytes } from "./bytes";
import { Value } from "./values";
import { Set, Dictionary } from "./dictionary";
import { annotate, Annotated } from "./annotated";
import { Double, Float, Single } from "./float";
import { Embedded } from "./embedded";

export enum ValueClass {
    Boolean,
    Float,
    Double,
    SignedInteger,
    String,
    ByteString,
    Symbol,
    Record,
    Sequence,
    Set,
    Dictionary,
    Embedded,
    Annotated, // quasi-class
}

export type Fold<T, R = Value<T>> = (v: Value<T>) => R;

export interface FoldMethods<T, R> {
    boolean(b: boolean): R;
    single(f: number): R;
    double(f: number): R;
    integer(i: number): R;
    string(s: string): R;
    bytes(b: Bytes): R;
    symbol(s: symbol): R;

    record(r: Record<Value<T>, Tuple<Value<T>>, T>, k: Fold<T, R>): R;
    array(a: Array<Value<T>>, k: Fold<T, R>): R;
    set(s: Set<T>, k: Fold<T, R>): R;
    dictionary(d: Dictionary<T>, k: Fold<T, R>): R;

    annotated(a: Annotated<T>, k: Fold<T, R>): R;

    embedded(t: Embedded<T>, k: Fold<T, R>): R;
}

export class VoidFold<T> implements FoldMethods<T, void> {
    boolean(b: boolean): void {}
    single(f: number): void {}
    double(f: number): void {}
    integer(i: number): void {}
    string(s: string): void {}
    bytes(b: Bytes): void {}
    symbol(s: symbol): void {}
    record(r: Record<Value<T>, Tuple<Value<T>>, T>, k: Fold<T, void>): void {
        k(r.label);
        r.forEach(k);
    }
    array(a: Value<T>[], k: Fold<T, void>): void { a.forEach(k); }
    set(s: Set<T>, k: Fold<T, void>): void { s.forEach(k); }
    dictionary(d: Dictionary<T>, k: Fold<T, void>): void {
        d.forEach((value, key) => { k(key); k(value); });
    }
    annotated(a: Annotated<T>, k: Fold<T, void>): void { k(a.item); a.annotations.forEach(k); }
    embedded(_t: Embedded<T>, _k: Fold<T, void>): void {}
}

export class ForEachEmbedded<T> extends VoidFold<T> {
    constructor(public readonly f: (t: T, k: Fold<T, void>) => void) { super(); }
    embedded(t: Embedded<T>, k: Fold<T, void>): void { this.f(t.embeddedValue, k); }
}

export abstract class ValueFold<T, R = T> implements FoldMethods<T, Value<R>> {
    boolean(b: boolean): Value<R> {
        return b;
    }
    single(f: number): Value<R> {
        return Single(f);
    }
    double(f: number): Value<R> {
        return Double(f);
    }
    integer(i: number): Value<R> {
        return i;
    }
    string(s: string): Value<R> {
        return s;
    }
    bytes(b: Bytes): Value<R> {
        return b;
    }
    symbol(s: symbol): Value<R> {
        return s;
    }
    record(r: Record<Value<T>, Tuple<Value<T>>, T>, k: Fold<T, Value<R>>): Value<R> {
        return Record(k(r.label), r.map(k));
    }
    array(a: Value<T>[], k: Fold<T, Value<R>>): Value<R> {
        return a.map(k);
    }
    set(s: Set<T>, k: Fold<T, Value<R>>): Value<R> {
        return s.map(k);
    }
    dictionary(d: Dictionary<T>, k: Fold<T, Value<R>>): Value<R> {
        return d.mapEntries(([key, value]) => [k(key), k(value)]);
    }
    annotated(a: Annotated<T>, k: Fold<T, Value<R>>): Value<R> {
        return annotate(k(a.item), ...a.annotations.map(k));
    }
    abstract embedded(t: Embedded<T>, k: Fold<T, Value<R>>): Value<R>;
}

export class IdentityFold<T> extends ValueFold<T, T> {
    embedded(t: Embedded<T>, _k: Fold<T, Value<T>>): Value<T> {
        return t;
    }
}

export class MapFold<T, R> extends ValueFold<T, R> {
    readonly f: (t: T) => Value<R>;

    constructor(f: (t: T) => Value<R>) {
        super();
        this.f = f;
    }

    embedded(t: Embedded<T>, _k: Fold<T, Value<R>>): Value<R> {
        return this.f(t.embeddedValue);
    }
}

export function valueClass<T>(v: Value<T>): ValueClass {
    switch (typeof v) {
        case 'boolean':
            return ValueClass.Boolean;
        case 'number':
            if (!Number.isInteger(v)) {
                throw new Error("Non-integer number in Preserves valueClass; missing SingleFloat/DoubleFloat wrapper?");
            } else {
                return ValueClass.SignedInteger;
            }
        case 'string':
            return ValueClass.String;
        case 'symbol':
            return ValueClass.Symbol;
        case 'object':
            if (Record.isRecord<Value<T>, Tuple<Value<T>>, T>(v)) {
                return ValueClass.Record;
            } else if (Array.isArray(v)) {
                return ValueClass.Sequence;
            } else if (Set.isSet<T>(v)) {
                return ValueClass.Set;
            } else if (Dictionary.isDictionary<T>(v)) {
                return ValueClass.Dictionary;
            } else if (Annotated.isAnnotated<T>(v)) {
                return ValueClass.Annotated;
            } else if (Bytes.isBytes(v)) {
                return ValueClass.ByteString;
            } else if (Float.isSingle(v)) {
                return ValueClass.Float;
            } else if (Float.isDouble(v)) {
                return ValueClass.Double;
            } else {
                return ValueClass.Embedded;
            }
        default:
            ((_v: never): never => { throw new Error("Internal error"); })(v);
    }
}

export const IDENTITY_FOLD = new IdentityFold<any>();

export function fold<T, R>(v: Value<T>, o: FoldMethods<T, R>): R {
    const walk = (v: Value<T>): R => {
        switch (typeof v) {
            case 'boolean':
                return o.boolean(v);
            case 'number':
                if (!Number.isInteger(v)) {
                    // TODO: Is this convenience warranted?
                    return o.double(v);
                } else {
                    return o.integer(v);
                }
            case 'string':
                return o.string(v);
            case 'symbol':
                return o.symbol(v);
            case 'object':
                if (Record.isRecord<Value<T>, Tuple<Value<T>>, T>(v)) {
                    return o.record(v, walk);
                } else if (Array.isArray(v)) {
                    return o.array(v, walk);
                } else if (Set.isSet<T>(v)) {
                    return o.set(v, walk);
                } else if (Dictionary.isDictionary<T>(v)) {
                    return o.dictionary(v, walk);
                } else if (Annotated.isAnnotated<T>(v)) {
                    return o.annotated(v, walk);
                } else if (Bytes.isBytes(v)) {
                    return o.bytes(v);
                } else if (Float.isSingle(v)) {
                    return o.single(v.value);
                } else if (Float.isDouble(v)) {
                    return o.double(v.value);
                } else {
                    return o.embedded(v, walk);
                }
            default:
                ((_v: never): never => { throw new Error("Internal error"); })(v);
        }
    };
    return walk(v);
}

export function mapEmbeddeds<T, R>(
    v: Value<T>,
    f: (t: T) => Value<R>,
): Value<R>
{
    return fold(v, new MapFold(f));
}

export function forEachEmbedded<T>(v: Value<T>, f: (t: T, k: Fold<T, void>) => void): void {
    return fold(v, new ForEachEmbedded(f));
}
