/* typehints:start */
import { GameRoot } from "../game/root";
import { BasicSerializableObject } from "./serialization";
/* typehints:end */

import { Vector } from "../core/vector";
import { round4Digits, schemaObject, accessNestedPropertyReverse } from "../core/utils";
import { JSON_stringify } from "../core/builtins";

export const globalJsonSchemaDefs = {};

/**
 *
 * @param {import("./serialization").Schema} schema
 */
export function schemaToJsonSchema(schema) {
    const jsonSchema = {
        type: "object",
        additionalProperties: false,
        required: [],
        properties: {},
    };

    for (const key in schema) {
        const subSchema = schema[key].getAsJsonSchema();
        jsonSchema.required.push(key);
        jsonSchema.properties[key] = subSchema;
    }

    return jsonSchema;
}

/**
 * Base serialization data type
 */
export class BaseDataType {
    /**
     * Serializes a given raw value
     * @param {any} value
     */
    serialize(value) {
        abstract;
        return {};
    }

    /**
     * Verifies a given serialized value
     * @param {any} value
     * @returns {string|void} String error code or null on success
     */
    verifySerializedValue(value) {}

    /**
     * Deserializes a serialized value into the target object under the given key
     * @param {any} value
     * @param {GameRoot} root
     * @param {object} targetObject
     * @param {string|number} targetKey
     * @returns {string|void} String error code or null on success
     */
    deserialize(value, targetObject, targetKey, root) {
        abstract;
    }

    /**
     * Returns the json schema
     */
    getAsJsonSchema() {
        const key = this.getCacheKey();
        const schema = this.getAsJsonSchemaUncached();

        if (!globalJsonSchemaDefs[key]) {
            // schema.$id = key;
            globalJsonSchemaDefs[key] = schema;
        }

        return {
            $ref: "#/definitions/" + key,
        };

        // return this.getAsJsonSchemaUncached();
        // if (!globalJsonSchemaDefs[key]) {
        //     // schema.$id = key;
        //     globalJsonSchemaDefs[key] = {
        //         $id: key,
        //         definitions: {
        //             ["d-" + key]: schema
        //         }
        //     };
        // }

        // return {
        //     $ref: key + "#/definitions/d-" + key
        // }

        // // return this.getAsJsonSchemaUncached();
    }

    /**
     * INTERNAL Should return the json schema representation
     */
    getAsJsonSchemaUncached() {
        abstract;
    }

    /**
     * Returns whether null values are okay
     * @returns {boolean}
     */
    allowNull() {
        return false;
    }

    // Helper methods

    /**
     * Deserializes a serialized value, but performs integrity checks before
     * @param {any} value
     * @param {GameRoot} root
     * @param {object} targetObject
     * @param {string|number} targetKey
     * @returns {string|void} String error code or null on success
     */
    deserializeWithVerify(value, targetObject, targetKey, root) {
        const errorCode = this.verifySerializedValue(value);
        if (errorCode) {
            return (
                "serialization verify failed: " +
                errorCode +
                " [value " +
                JSON_stringify(value).substr(0, 100) +
                "]"
            );
        }
        return this.deserialize(value, targetObject, targetKey, root);
    }

    /**
     * Should return a cacheable key
     */
    getCacheKey() {
        abstract;
        return "";
    }
}

export class TypeInteger extends BaseDataType {
    serialize(value) {
        assert(Number.isInteger(value), "Type integer got non integer for serialize: " + value);
        return value;
    }

    /**
     * @see BaseDataType.deserialize
     * @param {any} value
     * @param {GameRoot} root
     * @param {object} targetObject
     * @param {string|number} targetKey
     * @returns {string|void} String error code or null on success
     */
    deserialize(value, targetObject, targetKey, root) {
        targetObject[targetKey] = value;
    }

    getAsJsonSchemaUncached() {
        return {
            type: "integer",
        };
    }

    verifySerializedValue(value) {
        if (!Number.isInteger(value)) {
            return "Not a valid number";
        }
    }

    getCacheKey() {
        return "int";
    }
}

export class TypePositiveInteger extends BaseDataType {
    serialize(value) {
        assert(Number.isInteger(value), "Type integer got non integer for serialize: " + value);
        assert(value >= 0, "value < 0: " + value);
        return value;
    }

    /**
     * @see BaseDataType.deserialize
     * @param {any} value
     * @param {GameRoot} root
     * @param {object} targetObject
     * @param {string|number} targetKey
     * @returns {string|void} String error code or null on success
     */
    deserialize(value, targetObject, targetKey, root) {
        targetObject[targetKey] = value;
    }

    getAsJsonSchemaUncached() {
        return {
            type: "integer",
            minimum: 0,
        };
    }

    verifySerializedValue(value) {
        if (!Number.isInteger(value)) {
            return "Not a valid number";
        }
        if (value < 0) {
            return "Negative value for positive integer";
        }
    }

    getCacheKey() {
        return "uint";
    }
}

export class TypeBoolean extends BaseDataType {
    serialize(value) {
        assert(value === true || value === false, "Type bool got non bool for serialize: " + value);
        return value;
    }

    /**
     * @see BaseDataType.deserialize
     * @param {any} value
     * @param {GameRoot} root
     * @param {object} targetObject
     * @param {string|number} targetKey
     * @returns {string|void} String error code or null on success
     */
    deserialize(value, targetObject, targetKey, root) {
        targetObject[targetKey] = value;
    }

    getAsJsonSchemaUncached() {
        return {
            type: "boolean",
        };
    }

    verifySerializedValue(value) {
        if (value !== true && value !== false) {
            return "Not a boolean";
        }
    }

    getCacheKey() {
        return "bool";
    }
}

export class TypeString extends BaseDataType {
    serialize(value) {
        assert(typeof value === "string", "Type string got non string for serialize: " + value);
        return value;
    }

    /**
     * @see BaseDataType.deserialize
     * @param {any} value
     * @param {GameRoot} root
     * @param {object} targetObject
     * @param {string|number} targetKey
     * @returns {string|void} String error code or null on success
     */
    deserialize(value, targetObject, targetKey, root) {
        targetObject[targetKey] = value;
    }
    getAsJsonSchemaUncached() {
        return {
            type: "string",
        };
    }

    verifySerializedValue(value) {
        if (typeof value !== "string") {
            return "Not a valid string";
        }
    }

    getCacheKey() {
        return "string";
    }
}

export class TypeVector extends BaseDataType {
    serialize(value) {
        assert(value instanceof Vector, "Type vector got non vector for serialize: " + value);
        return {
            x: round4Digits(value.x),
            y: round4Digits(value.y),
        };
    }

    getAsJsonSchemaUncached() {
        return schemaObject({
            x: {
                type: "number",
            },
            y: {
                type: "number",
            },
        });
    }

    /**
     * @see BaseDataType.deserialize
     * @param {any} value
     * @param {GameRoot} root
     * @param {object} targetObject
     * @param {string|number} targetKey
     * @returns {string|void} String error code or null on success
     */
    deserialize(value, targetObject, targetKey, root) {
        targetObject[targetKey] = new Vector(value.x, value.y);
    }

    verifySerializedValue(value) {
        if (!Number.isFinite(value.x) || !Number.isFinite(value.y)) {
            return "Not a valid vector, missing x/y or bad data type";
        }
    }

    getCacheKey() {
        return "vector";
    }
}

export class TypeTileVector extends BaseDataType {
    serialize(value) {
        assert(value instanceof Vector, "Type vector got non vector for serialize: " + value);
        assert(Number.isInteger(value.x) && value.x > 0, "Invalid tile x:" + value.x);
        assert(Number.isInteger(value.y) && value.y > 0, "Invalid tile x:" + value.y);
        return { x: value.x, y: value.y };
    }

    getAsJsonSchemaUncached() {
        return schemaObject({
            x: {
                type: "integer",
                minimum: 0,
                maximum: 256,
            },
            y: {
                type: "integer",
                minimum: 0,
                maximum: 256,
            },
        });
    }

    /**
     * @see BaseDataType.deserialize
     * @param {any} value
     * @param {GameRoot} root
     * @param {object} targetObject
     * @param {string|number} targetKey
     * @returns {string|void} String error code or null on success
     */
    deserialize(value, targetObject, targetKey, root) {
        targetObject[targetKey] = new Vector(value.x, value.y);
    }

    verifySerializedValue(value) {
        if (!Number.isInteger(value.x) || !Number.isInteger(value.y)) {
            return "Not a valid tile vector, missing x/y or bad data type";
        }
        if (value.x < 0 || value.y < 0) {
            return "Invalid tile vector, x or y < 0";
        }
    }

    getCacheKey() {
        return "tilevector";
    }
}

export class TypeNumber extends BaseDataType {
    serialize(value) {
        assert(Number.isFinite(value), "Type number got non number for serialize: " + value);
        assert(!Number.isNaN(value), "Value is nan: " + value);
        return round4Digits(value);
    }

    getAsJsonSchemaUncached() {
        return {
            type: "number",
        };
    }

    /**
     * @see BaseDataType.deserialize
     * @param {any} value
     * @param {GameRoot} root
     * @param {object} targetObject
     * @param {string|number} targetKey
     * @returns {string|void} String error code or null on success
     */
    deserialize(value, targetObject, targetKey, root) {
        targetObject[targetKey] = value;
    }

    verifySerializedValue(value) {
        if (!Number.isFinite(value)) {
            return "Not a valid number: " + value;
        }
    }

    getCacheKey() {
        return "float";
    }
}

export class TypePositiveNumber extends BaseDataType {
    serialize(value) {
        assert(Number.isFinite(value), "Type number got non number for serialize: " + value);
        assert(value >= 0, "Postitive number got negative value: " + value);
        return round4Digits(value);
    }

    /**
     * @see BaseDataType.deserialize
     * @param {any} value
     * @param {GameRoot} root
     * @param {object} targetObject
     * @param {string|number} targetKey
     * @returns {string|void} String error code or null on success
     */
    deserialize(value, targetObject, targetKey, root) {
        targetObject[targetKey] = value;
    }

    getAsJsonSchemaUncached() {
        return {
            type: "number",
            minimum: 0,
        };
    }

    verifySerializedValue(value) {
        if (!Number.isFinite(value)) {
            return "Not a valid number: " + value;
        }
        if (value < 0) {
            return "Positive number got negative value: " + value;
        }
    }

    getCacheKey() {
        return "ufloat";
    }
}

export class TypeEnum extends BaseDataType {
    /**
     * @param {Object.<string, any>} enumeration
     */
    constructor(enumeration = {}) {
        super();
        this.availableValues = Object.keys(enumeration);
    }

    serialize(value) {
        assert(this.availableValues.indexOf(value) >= 0, "Unknown value: " + value);
        return value;
    }

    /**
     * @see BaseDataType.deserialize
     * @param {any} value
     * @param {GameRoot} root
     * @param {object} targetObject
     * @param {string|number} targetKey
     * @returns {string|void} String error code or null on success
     */
    deserialize(value, targetObject, targetKey, root) {
        targetObject[targetKey] = value;
    }

    getAsJsonSchemaUncached() {
        return {
            type: "string",
            enum: this.availableValues,
        };
    }

    verifySerializedValue(value) {
        if (this.availableValues.indexOf(value) < 0) {
            return "Unknown enum value: " + value;
        }
    }

    getCacheKey() {
        return "enum." + this.availableValues.join(",");
    }
}

export class TypeEntity extends BaseDataType {
    serialize(value) {
        // assert(value instanceof Entity, "Not a valid entity ref: " + value);
        assert(value.uid, "Entity has no uid yet");
        assert(!value.destroyed, "Entity already destroyed");
        assert(!value.queuedForDestroy, "Entity queued for destroy");

        return value.uid;
    }

    getAsJsonSchemaUncached() {
        return {
            type: "integer",
            minimum: 0,
        };
    }

    /**
     * @see BaseDataType.deserialize
     * @param {any} value
     * @param {GameRoot} root
     * @param {object} targetObject
     * @param {string|number} targetKey
     * @returns {string|void} String error code or null on success
     */
    deserialize(value, targetObject, targetKey, root) {
        const entity = root.entityMgr.findByUid(value);
        if (!entity) {
            return "Entity not found by uid: " + value;
        }
        targetObject[targetKey] = entity;
    }

    verifySerializedValue(value) {
        if (!Number.isFinite(value)) {
            return "Not a valid uuid: " + value;
        }
    }

    getCacheKey() {
        return "entity";
    }
}

export class TypeEntityWeakref extends BaseDataType {
    serialize(value) {
        if (value === null) {
            return null;
        }

        // assert(value instanceof Entity, "Not a valid entity ref (weak): " + value);
        assert(value.uid, "Entity has no uid yet");
        if (value.destroyed || value.queuedForDestroy) {
            return null;
        }
        return value.uid;
    }

    /**
     * @see BaseDataType.deserialize
     * @param {any} value
     * @param {GameRoot} root
     * @param {object} targetObject
     * @param {string|number} targetKey
     * @returns {string|void} String error code or null on success
     */
    deserialize(value, targetObject, targetKey, root) {
        if (value === null) {
            targetObject[targetKey] = null;
            return;
        }
        const entity = root.entityMgr.findByUid(value, false);
        targetObject[targetKey] = entity;
    }

    getAsJsonSchemaUncached() {
        return {
            type: ["null", "integer"],
            minimum: 0,
        };
    }

    allowNull() {
        return true;
    }

    verifySerializedValue(value) {
        if (value !== null && !Number.isFinite(value)) {
            return "Not a valid uuid: " + value;
        }
    }

    getCacheKey() {
        return "entity-weakref";
    }
}

export class TypeClass extends BaseDataType {
    /**
     *
     * @param {FactoryTemplate<*>} registry
     */
    constructor(registry) {
        super();
        this.registry = registry;
    }

    serialize(value) {
        assert(typeof value === "object", "Not a class instance: " + value);
        return {
            $: value.constructor.getId(),
            data: value.serialize(),
        };
    }

    getAsJsonSchemaUncached() {
        const options = [];
        const entries = this.registry.getEntries();
        for (let i = 0; i < entries.length; ++i) {
            const entry = entries[i];

            options.push(
                schemaObject({
                    $: {
                        type: "string",
                        // @ts-ignore
                        enum: [entry.getId()],
                    },
                    // @ts-ignore
                    data: schemaToJsonSchema(entry.getCachedSchema()),
                })
            );
        }

        return { oneOf: options };
    }

    /**
     * @see BaseDataType.deserialize
     * @param {any} value
     * @param {GameRoot} root
     * @param {object} targetObject
     * @param {string|number} targetKey
     * @returns {string|void} String error code or null on success
     */
    deserialize(value, targetObject, targetKey, root) {
        const instanceClass = this.registry.findById(value.$);
        if (!instanceClass || !instanceClass.prototype) {
            return "Invalid class id (runtime-err): " + value.$ + "->" + instanceClass;
        }
        const instance = Object.create(instanceClass.prototype);
        const errorState = instance.deserialize(value.data);
        if (errorState) {
            return errorState;
        }
        targetObject[targetKey] = instance;
    }

    verifySerializedValue(value) {
        if (!value) {
            return "Got null data";
        }

        if (!this.registry.hasId(value.$)) {
            return "Invalid class id: " + value.$ + " (factory is " + this.registry.getId() + ")";
        }
    }

    getCacheKey() {
        return "class." + this.registry.getId();
    }
}

export class TypeClassData extends BaseDataType {
    /**
     *
     * @param {FactoryTemplate<*>} registry
     */
    constructor(registry) {
        super();
        this.registry = registry;
    }

    serialize(value) {
        assert(typeof value === "object", "Not a class instance: " + value);
        return value.serialize();
    }

    getAsJsonSchemaUncached() {
        const options = [];
        const entries = this.registry.getEntries();
        for (let i = 0; i < entries.length; ++i) {
            const entry = entries[i];
            options.push(
                schemaToJsonSchema(/** @type {typeof BasicSerializableObject} */ (entry).getCachedSchema())
            );
        }
        return { oneOf: options };
    }

    /**
     * @see BaseDataType.deserialize
     * @param {any} value
     * @param {GameRoot} root
     * @param {object} targetObject
     * @param {string|number} targetKey
     * @returns {string|void} String error code or null on success
     */
    deserialize(value, targetObject, targetKey, root) {
        assert(false, "can not deserialize class data of type " + this.registry.getId());
    }

    verifySerializedValue(value) {
        if (!value) {
            return "Got null data";
        }
    }

    getCacheKey() {
        return "class." + this.registry.getId();
    }
}

export class TypeClassFromMetaclass extends BaseDataType {
    /**
     *
     * @param {typeof BasicSerializableObject} classHandle
     * @param {SingletonFactoryTemplate<*>} registry
     */
    constructor(classHandle, registry) {
        super();
        this.registry = registry;
        this.classHandle = classHandle;
    }

    serialize(value) {
        assert(typeof value === "object", "Not a class instance: " + value);
        return {
            $: value.getMetaclass().getId(),
            data: value.serialize(),
        };
    }

    getAsJsonSchemaUncached() {
        // const options = [];
        const ids = this.registry.getAllIds();

        return {
            $: {
                type: "string",
                enum: ids,
            },
            data: schemaToJsonSchema(this.classHandle.getCachedSchema()),
        };
    }

    /**
     * @see BaseDataType.deserialize
     * @param {any} value
     * @param {GameRoot} root
     * @param {object} targetObject
     * @param {string|number} targetKey
     * @returns {string|void} String error code or null on success
     */
    deserialize(value, targetObject, targetKey, root) {
        const metaClassInstance = this.registry.findById(value.$);
        if (!metaClassInstance || !metaClassInstance.prototype) {
            return "Invalid meta class id (runtime-err): " + value.$ + "->" + metaClassInstance;
        }

        const instanceClass = metaClassInstance.getInstanceClass();
        const instance = Object.create(instanceClass.prototype);
        const errorState = instance.deserialize(value.data);
        if (errorState) {
            return errorState;
        }
        targetObject[targetKey] = instance;
    }

    verifySerializedValue(value) {
        if (!value) {
            return "Got null data";
        }

        if (!this.registry.hasId(value.$)) {
            return "Invalid class id: " + value.$ + " (factory is " + this.registry.getId() + ")";
        }
    }

    getCacheKey() {
        return "classofmetaclass." + this.registry.getId();
    }
}

export class TypeMetaClass extends BaseDataType {
    /**
     *
     * @param {SingletonFactoryTemplate<*>} registry
     */
    constructor(registry) {
        super();
        this.registry = registry;
    }

    serialize(value) {
        return value.getId();
    }

    /**
     * @see BaseDataType.deserialize
     * @param {any} value
     * @param {GameRoot} root
     * @param {object} targetObject
     * @param {string|number} targetKey
     * @returns {string|void} String error code or null on success
     */
    deserialize(value, targetObject, targetKey, root) {
        const instanceClass = this.registry.findById(value);
        if (!instanceClass) {
            return "Invalid class id (runtime-err): " + value;
        }
        targetObject[targetKey] = instanceClass;
    }

    getAsJsonSchemaUncached() {
        return {
            type: "string",
            enum: this.registry.getAllIds(),
        };
    }

    verifySerializedValue(value) {
        if (!value) {
            return "Got null data";
        }

        if (typeof value !== "string") {
            return "Got non string data";
        }

        if (!this.registry.hasId(value)) {
            return "Invalid class id: " + value + " (factory is " + this.registry.getId() + ")";
        }
    }

    getCacheKey() {
        return "metaclass." + this.registry.getId();
    }
}

export class TypeArray extends BaseDataType {
    /**
     * @param {BaseDataType} innerType
     */
    constructor(innerType) {
        super();
        this.innerType = innerType;
    }

    serialize(value) {
        assert(Array.isArray(value), "Not an array");
        const result = new Array(value.length);
        for (let i = 0; i < value.length; ++i) {
            result[i] = this.innerType.serialize(value[i]);
        }
        return result;
    }

    /**
     * @see BaseDataType.deserialize
     * @param {any} value
     * @param {GameRoot} root
     * @param {object} targetObject
     * @param {string|number} targetKey
     * @returns {string|void} String error code or null on success
     */
    deserialize(value, targetObject, targetKey, root) {
        const result = new Array(value.length);
        for (let i = 0; i < value.length; ++i) {
            const errorStatus = this.innerType.deserializeWithVerify(value[i], result, i, root);
            if (errorStatus) {
                return errorStatus;
            }
        }
        targetObject[targetKey] = result;
    }

    getAsJsonSchemaUncached() {
        return {
            type: "array",
            items: this.innerType.getAsJsonSchema(),
        };
    }

    verifySerializedValue(value) {
        if (!Array.isArray(value)) {
            return "Not an array: " + value;
        }
    }

    getCacheKey() {
        return "array." + this.innerType.getCacheKey();
    }
}

export class TypeFixedClass extends BaseDataType {
    /**
     *
     * @param {typeof BasicSerializableObject} baseclass
     */
    constructor(baseclass) {
        super();
        this.baseclass = baseclass;
    }

    serialize(value) {
        assert(value instanceof this.baseclass, "Not a valid class instance");
        return value.serialize();
    }

    /**
     * @see BaseDataType.deserialize
     * @param {any} value
     * @param {GameRoot} root
     * @param {object} targetObject
     * @param {string|number} targetKey
     * @returns {string|void} String error code or null on success
     */
    deserialize(value, targetObject, targetKey, root) {
        const instance = Object.create(this.baseclass.prototype);
        const errorState = instance.deserialize(value);
        if (errorState) {
            return "Failed to deserialize class: " + errorState;
        }
        targetObject[targetKey] = instance;
    }

    getAsJsonSchemaUncached() {
        this.baseclass.getSchema();
        this.baseclass.getCachedSchema();
        return schemaToJsonSchema(this.baseclass.getCachedSchema());
    }

    verifySerializedValue(value) {
        if (!value) {
            return "Got null data";
        }
    }

    getCacheKey() {
        return "fixedclass." + this.baseclass.getId();
    }
}

export class TypeKeyValueMap extends BaseDataType {
    /**
     * @param {BaseDataType} valueType
     * @param {boolean=} includeEmptyValues
     */
    constructor(valueType, includeEmptyValues = true) {
        super();
        this.valueType = valueType;
        this.includeEmptyValues = includeEmptyValues;
    }

    serialize(value) {
        assert(typeof value === "object", "not an object");
        let result = {};
        for (const key in value) {
            const serialized = this.valueType.serialize(value[key]);
            if (!this.includeEmptyValues && typeof serialized === "object") {
                if (
                    serialized.$ &&
                    typeof serialized.data === "object" &&
                    Object.keys(serialized.data).length === 0
                ) {
                    continue;
                } else if (Object.keys(serialized).length === 0) {
                    continue;
                }
            }

            result[key] = serialized;
        }
        return result;
    }

    /**
     * @see BaseDataType.deserialize
     * @param {any} value
     * @param {GameRoot} root
     * @param {object} targetObject
     * @param {string|number} targetKey
     * @returns {string|void} String error code or null on success
     */
    deserialize(value, targetObject, targetKey, root) {
        let result = {};
        for (const key in value) {
            const errorCode = this.valueType.deserializeWithVerify(value[key], result, key, root);
            if (errorCode) {
                return errorCode;
            }
        }
        targetObject[targetKey] = result;
    }

    getAsJsonSchemaUncached() {
        return {
            type: "object",
            additionalProperties: this.valueType.getAsJsonSchema(),
        };
    }

    verifySerializedValue(value) {
        if (typeof value !== "object") {
            return "KV map is not an object";
        }
    }

    getCacheKey() {
        return "kvmap." + this.valueType.getCacheKey();
    }
}

export class TypeClassId extends BaseDataType {
    /**
     * @param {FactoryTemplate<*>|SingletonFactoryTemplate<*>} registry
     */
    constructor(registry) {
        super();
        this.registry = registry;
    }

    serialize(value) {
        assert(typeof value === "string", "Not a valid string");
        assert(this.registry.hasId(value), "Id " + value + " not found in registry");
        return value;
    }

    /**
     * @see BaseDataType.deserialize
     * @param {any} value
     * @param {GameRoot} root
     * @param {object} targetObject
     * @param {string|number} targetKey
     * @returns {string|void} String error code or null on success
     */
    deserialize(value, targetObject, targetKey, root) {
        targetObject[targetKey] = value;
    }

    getAsJsonSchemaUncached() {
        return {
            type: "string",
            enum: this.registry.getAllIds(),
        };
    }

    verifySerializedValue(value) {
        if (typeof value !== "string") {
            return "Not a valid registry id key: " + value;
        }
        if (!this.registry.hasId(value)) {
            return "Id " + value + " not known to registry";
        }
    }

    getCacheKey() {
        return "classid." + this.registry.getId();
    }
}

export class TypePair extends BaseDataType {
    /**
     * @param {BaseDataType} type1
     * @param {BaseDataType} type2
     */
    constructor(type1, type2) {
        super();
        assert(type1 && type1 instanceof BaseDataType, "bad first type given for pair");
        assert(type2 && type2 instanceof BaseDataType, "bad second type given for pair");
        this.type1 = type1;
        this.type2 = type2;
    }

    serialize(value) {
        assert(Array.isArray(value), "pair: not an array");
        assert(value.length === 2, "pair: length != 2");
        return [this.type1.serialize(value[0]), this.type2.serialize(value[1])];
    }

    /**
     * @see BaseDataType.deserialize
     * @param {any} value
     * @param {GameRoot} root
     * @param {object} targetObject
     * @param {string|number} targetKey
     * @returns {string|void} String error code or null on success
     */
    deserialize(value, targetObject, targetKey, root) {
        const result = [undefined, undefined];

        let errorCode = this.type1.deserialize(value[0], result, 0, root);
        if (errorCode) {
            return errorCode;
        }
        errorCode = this.type2.deserialize(value[1], result, 1, root);
        if (errorCode) {
            return errorCode;
        }

        targetObject[targetKey] = result;
    }

    getAsJsonSchemaUncached() {
        return {
            type: "array",
            minLength: 2,
            maxLength: 2,
            items: [this.type1.getAsJsonSchema(), this.type2.getAsJsonSchema()],
        };
    }

    verifySerializedValue(value) {
        if (!Array.isArray(value)) {
            return "Pair is not an array";
        }
        if (value.length !== 2) {
            return "Pair length != 2";
        }
        let errorCode = this.type1.verifySerializedValue(value[0]);
        if (errorCode) {
            return errorCode;
        }
        errorCode = this.type2.verifySerializedValue(value[1]);
        if (errorCode) {
            return errorCode;
        }
    }

    getCacheKey() {
        return "pair.(" + this.type1.getCacheKey() + "," + this.type2.getCacheKey + ")";
    }
}

export class TypeNullable extends BaseDataType {
    /**
     * @param {BaseDataType} wrapped
     */
    constructor(wrapped) {
        super();
        this.wrapped = wrapped;
    }

    serialize(value) {
        if (value === null || value === undefined) {
            return null;
        }
        return this.wrapped.serialize(value);
    }

    /**
     * @see BaseDataType.deserialize
     * @param {any} value
     * @param {GameRoot} root
     * @param {object} targetObject
     * @param {string|number} targetKey
     * @returns {string|void} String error code or null on success
     */
    deserialize(value, targetObject, targetKey, root) {
        if (value === null || value === undefined) {
            targetObject[targetKey] = null;
            return;
        }
        return this.wrapped.deserialize(value, targetObject, targetKey, root);
    }

    verifySerializedValue(value) {
        if (value === null) {
            return;
        }
        return this.wrapped.verifySerializedValue(value);
    }

    getAsJsonSchemaUncached() {
        return {
            oneOf: [
                {
                    type: "null",
                },
                this.wrapped.getAsJsonSchema(),
            ],
        };
    }

    allowNull() {
        return true;
    }

    getCacheKey() {
        return "nullable." + this.wrapped.getCacheKey();
    }
}

export class TypeStructuredObject extends BaseDataType {
    /**
     * @param {Object.<string, BaseDataType>} descriptor
     */
    constructor(descriptor) {
        super();
        this.descriptor = descriptor;
    }

    serialize(value) {
        assert(typeof value === "object", "not an object");
        let result = {};
        for (const key in this.descriptor) {
            // assert(value.hasOwnProperty(key), "Serialization: Object does not have", key, "property!");
            result[key] = this.descriptor[key].serialize(value[key]);
        }
        return result;
    }

    /**
     * @see BaseDataType.deserialize
     * @param {any} value
     * @param {GameRoot} root
     * @param {object} targetObject
     * @param {string|number} targetKey
     * @returns {string|void} String error code or null on success
     */
    deserialize(value, targetObject, targetKey, root) {
        let result = {};
        for (const key in value) {
            const valueType = this.descriptor[key];
            const errorCode = valueType.deserializeWithVerify(value[key], result, key, root);
            if (errorCode) {
                return errorCode;
            }
        }
        targetObject[targetKey] = result;
    }

    getAsJsonSchemaUncached() {
        let properties = {};
        for (const key in this.descriptor) {
            properties[key] = this.descriptor[key].getAsJsonSchema();
        }

        return {
            type: "object",
            required: Object.keys(this.descriptor),
            properties,
        };
    }

    verifySerializedValue(value) {
        if (typeof value !== "object") {
            return "structured object is not an object";
        }
        for (const key in this.descriptor) {
            if (!value.hasOwnProperty(key)) {
                return "structured object is missing key " + key;
            }
            const subError = this.descriptor[key].verifySerializedValue(value[key]);
            if (subError) {
                return "structured object::" + subError;
            }
        }
    }

    getCacheKey() {
        let props = [];
        for (const key in this.descriptor) {
            props.push(key + "=" + this.descriptor[key].getCacheKey());
        }
        return "structured[" + props.join(",") + "]";
    }
}
