import { Tag } from "./constants";
import { stringify } from "./text";
import { Value } from "./values";
import type { GenericEmbedded } from "./embedded";
import type { Encoder, Preservable } from "./encoder";
import type { Writer, PreserveWritable } from "./writer";
import { Bytes, dataview, underlying } from "./bytes";

export type FloatType = 'Single' | 'Double';
export const FloatType = Symbol.for('FloatType');

export abstract class Float {
    readonly value: number;

    constructor(value: number | Float) {
        this.value = typeof value === 'number' ? value : value.value;
    }

    toString() {
        return stringify(this);
    }

    abstract toBytes(): Bytes;

    equals(other: any): boolean {
        if (!Object.is(other.constructor, this.constructor)) return false;
        if (Number.isNaN(this.value) && Number.isNaN(other.value)) {
            return other.toBytes().equals(this.toBytes());
        } else {
            return Object.is(other.value, this.value);
        }
    }

    hashCode(): number {
        return (this.value | 0); // TODO: something better?
    }

    abstract get [FloatType](): FloatType;

    static isFloat = (x: any): x is Float => x?.[FloatType] !== void 0;
    static isSingle = (x: any): x is SingleFloat => x?.[FloatType] === 'Single';
    static isDouble = (x: any): x is DoubleFloat => x?.[FloatType] === 'Double';
}

export function floatValue(f: any): number {
    if (typeof f === 'number') {
        return f;
    } else if (Float.isFloat(f)) {
        return f.value;
    } else {
        return NaN;
    }
}

export function floatlikeString(f: number): string {
    if (Object.is(f, -0)) return '-0.0';
    const s = '' + f;
    if (s.includes('.') || s.includes('e') || s.includes('E')) return s;
    return s + '.0';
}

export class SingleFloat extends Float implements Preservable<any>, PreserveWritable<any> {
    __as_preserve__<T = GenericEmbedded>(): Value<T> {
        return this;
    }

    static fromBytes(bs: Bytes | DataView): SingleFloat {
        const view = dataview(bs);
        const vf = view.getInt32(0, false);
        if ((vf & 0x7f800000) === 0x7f800000) {
            // NaN or inf. Preserve quiet/signalling bit by manually expanding to double-precision.
            const sign = vf >> 31;
            const payload = vf & 0x007fffff;
            const dbs = new Bytes(8);
            const dview = dataview(dbs);
            dview.setInt16(0, (sign << 15) | 0x7ff0 | (payload >> 19), false);
            dview.setInt32(2, (payload & 0x7ffff) << 13, false);
            return new SingleFloat(dview.getFloat64(0, false));
        } else {
            return new SingleFloat(dataview(bs).getFloat32(0, false));
        }
    }

    static __from_preserve__<T>(v: Value<T>): undefined | SingleFloat {
        return Float.isSingle(v) ? v : void 0;
    }

    __w(v: DataView, offset: number) {
        if (Number.isNaN(this.value)) {
            const dbs = new Bytes(8);
            const dview = dataview(dbs);
            dview.setFloat64(0, this.value, false);
            const sign = dview.getInt8(0) >> 7;
            const payload = (dview.getInt32(1, false) >> 5) & 0x007fffff;
            const vf = (sign << 31) | 0x7f800000 | payload;
            v.setInt32(offset, vf, false);
        } else {
            v.setFloat32(offset, this.value, false);
        }
    }

    __preserve_on__(encoder: Encoder<any>) {
        encoder.state.emitbyte(Tag.Float);
        encoder.state.makeroom(4);
        this.__w(encoder.state.view, encoder.state.index);
        encoder.state.index += 4;
    }

    toBytes(): Bytes {
        const bs = new Bytes(4);
        this.__w(bs.dataview(), 0);
        return bs;
    }

    __preserve_text_on__(w: Writer<any>) {
        if (Number.isFinite(this.value)) {
            w.state.pieces.push(floatlikeString(this.value) + 'f');
        } else {
            w.state.pieces.push('#xf"', this.toBytes().toHex(), '"');
        }
    }

    get [FloatType](): 'Single' {
        return 'Single';
    }
}

export function Single(value: number | Float): SingleFloat {
    return new SingleFloat(value);
}

export class DoubleFloat extends Float implements Preservable<any>, PreserveWritable<any> {
    __as_preserve__<T = GenericEmbedded>(): Value<T> {
        return this;
    }

    static fromBytes(bs: Bytes | DataView): DoubleFloat {
        return new DoubleFloat(dataview(bs).getFloat64(0, false));
    }

    static __from_preserve__<T>(v: Value<T>): undefined | DoubleFloat {
        return Float.isDouble(v) ? v : void 0;
    }

    __preserve_on__(encoder: Encoder<any>) {
        encoder.state.emitbyte(Tag.Double);
        encoder.state.makeroom(8);
        encoder.state.view.setFloat64(encoder.state.index, this.value, false);
        encoder.state.index += 8;
    }

    toBytes(): Bytes {
        const bs = new Bytes(8);
        bs.dataview().setFloat64(0, this.value, false);
        return bs;
    }

    __preserve_text_on__(w: Writer<any>) {
        if (Number.isFinite(this.value)) {
            w.state.pieces.push(floatlikeString(this.value));
        } else {
            w.state.pieces.push('#xd"', this.toBytes().toHex(), '"');
        }
    }

    get [FloatType](): 'Double' {
        return 'Double';
    }
}

export function Double(value: number | Float): DoubleFloat {
    return new DoubleFloat(value);
}
