core/VarvEngine.js

/**
 *  VarvEngine - The core of the Varv system
 * 
 *  This code is licensed under the MIT License (MIT).
 *  
 *  Copyright 2020, 2021, 2022 Rolf Bagge, Janus B. Kristensen, CAVI,
 *  Center for Advanced Visualization and Interaction, Aarhus University
 *  
 *  Permission is hereby granted, free of charge, to any person obtaining a copy
 *  of this software and associated documentation files (the “Software”), to deal
 *  in the Software without restriction, including without limitation the rights 
 *  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 
 *  copies of the Software, and to permit persons to whom the Software is 
 *  furnished to do so, subject to the following conditions:
 *  
 *  The above copyright notice and this permission notice shall be included in 
 *  all copies or substantial portions of the Software.
 *  
 *  THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 
 *  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 
 *  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 
 *  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 
 *  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 
 *  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 
 *  THE SOFTWARE.
 */

/**
 * @private
 */

const RELOAD_TIMEOUT = 1000;

class VarvEngine {
    static getConceptFromUUID(uuid) {
        let mark = VarvPerformance.start();

        let cachedConcept = VarvEngine.conceptUUIDCache.get(uuid);
        if(cachedConcept != null) {
            VarvPerformance.stop("VarvEngine.getConceptFromUUID.cached", mark);
            return cachedConcept;
        }

        return new Promise(async (resolve, reject)=>{
            let promises = [];

            try {
                for(let ds of Datastore.getAllDatastores()) {
                    promises.push(ds.lookupConcept(uuid));
                }

                let result = await Promise.any(promises);

                if(result != null) {
                    VarvEngine.conceptUUIDCache.set(uuid, result);
                }

                VarvPerformance.stop("VarvEngine.getConceptFromUUID.nonCached", mark);

                resolve(result);
            } catch(e) {
                VarvPerformance.stop("VarvEngine.getConceptFromUUID.nonCached.error", mark);

                resolve(null);
            }
        });
    }

    static async switchConceptType(uuid, newConcept, oldConcept) {
        console.groupCollapsed("%c EXPERIMENTAL: Switching "+uuid+" from "+oldConcept.name+" to "+newConcept.name, "background: red; color: yellow");

        //Find and save all properties of the concept
        let oldProperties = {};

        for(let property of oldConcept.properties) {
            property = property[1];
            oldProperties[property.name] = {
                "type": property.type,
                "value": await property.getValue(uuid)
            }

            property.purgeCache(uuid);
        }

        console.log("Old properties:", oldProperties);

        //Dissapear the instance
        await oldConcept.disappeared(uuid);

        console.log("Creating instance as new concept!");
        VarvEngine.registerConceptFromUUID(uuid, newConcept);

        //Appear new instance
        await newConcept.appeared(uuid);

        //Set all properties that match
        for(let property of newConcept.properties) {
            property = property[1];

            if(oldProperties[property.name] != null) {
                let oldProperty = oldProperties[property.name];
                console.log("Matching property name:", oldProperty);
                try {
                    await property.setValue(uuid, oldProperty.value);
                } catch(e) {
                    console.warn("Unable to set property on new concept type:", e);
                }
            }
        }

        console.log("Cleaning references:");

        //Clean all references that no longer match
        for(let concept of VarvEngine.concepts) {
            for(let [key, property] of concept.properties) {
                //If property could hold old concept type, but not new, remove if our reference exists
                if(property.holdsConceptOfType(oldConcept) && !property.holdsConceptOfType(newConcept)) {
                    console.log("Found property that can no longer hold our reference:", property.name);
                    await property.removeAllReferences(concept.name, uuid);
                }
            }
        }

        console.groupEnd();
    }


    static getConceptFromType(type) {
        return VarvEngine.conceptTypeMap.get(type);
    }

    static async countInstances(typeNames, query, context, localConcept) {
        //TODO: What is correct here?

        let maxCount = 0;

        for(let datastore of Datastore.getAllDatastores()) {
            let count = await datastore.countInstances(typeNames, query, context, localConcept);
            maxCount = Math.max(count, maxCount);
        }

        return maxCount;
    }

    static async existsInstance(typeNames, query, context, localConcept) {
        //TODO: What is correct here?

        let exists = false;
        for(let datastore of Datastore.getAllDatastores()) {
            let result = await datastore.existsInstance(typeNames, query, context, localConcept);
            if(result) {
                exists = true;
                break;
            }
        }

        return exists;
    }

    /**
     *
     * @param {String|String[]}typeNames
     * @param {Filter} query
     * @param {VarvContext} context
     * @param {number}limit
     * @param {Concept} localConcept
     * @returns {Promise<any[]>}
     */
    static async lookupInstances(typeNames, query, context, limit = 0, localConcept = null) {
        if(!Array.isArray(typeNames)) {
            typeNames = [typeNames];
        }

        const mark = VarvPerformance.start();

        let result = new Set();
        let promises = [];
        for(let ds of Datastore.getAllDatastores()) {
            promises.push(ds.lookupInstances(typeNames, query, context, limit, localConcept));
        }

        let promiseResults = await Promise.all(promises);

        promiseResults.forEach((uuids)=>{
            if(uuids != null) {
                uuids.forEach((uuid)=>{
                    result.add(uuid);
                });
            }
        });
        VarvPerformance.stop("VarvEngine.lookupInstances", mark, {typeNames, query, limit});

        return Array.from(result);
    }

    static async getAllUUIDsFromType(type, includeOtherConcepts=false) {
        if(VarvEngine.getAllUUIDsFromTypeWarningShowed !== true) {
            console.groupCollapsed("%c DEPRECATED: [getAllUUIDsFromType] - consider rewriting ", "background: yellow; color: red;");
            console.trace();
            console.groupEnd();
            VarvEngine.getAllUUIDsFromTypeWarningShowed = true;
        }

        const mark = VarvPerformance.start()

        let uuidSet = new Set();

        let lookupTypes = [type];

        if(includeOtherConcepts) {
            lookupTypes = VarvEngine.getAllImplementingConceptNames(type);
        }

        for(let ds of Datastore.getAllDatastores()) {
            if(lookupTypes.find((type)=>{
                return ds.isConceptTypeMapped(type);
            }) != null) {
                if(! (ds instanceof DirectDatastore)) {
                    console.warn("getAllUUIDsFromType called on concept stored inside non DirectDatastore, potentially risky!");
                }

                //Pretend filtering is not a thing atm
                let uuids = await ds.lookupInstances(lookupTypes, null, null, 0, null);
                uuids.forEach((uuid) => {
                    uuidSet.add(uuid);
                });
            }
        }

        VarvPerformance.stop("VarvEngine.getAllUUIDsFromType", mark, {conceptType: type});

        return Array.from(uuidSet);
    }

    static getAllImplementingConceptNames(primaryType) {
        return VarvEngine.getAllImplementingConcepts(primaryType).map((concept)=>{
            return concept.name;
        });
    }

    /**
     * Returns an array of Concept that implement the given type, starting with 
     * the primary type itself
     * @param {type} primaryTypeName
     * @returns {Array}
     */
    static getAllImplementingConcepts(primaryTypeName){

        // Find all concepts with type, including other concepts
        let primaryConcept = null;
        let otherConcepts = VarvEngine.concepts.filter((concept)=>{
            if(concept.name === primaryTypeName) {
                primaryConcept = concept;
                return false; // This is prepended later
            }

            if(concept.otherConcepts.has(primaryTypeName)) {
                return true;
            }

            return false;
        }); 

        if(primaryConcept != null) {
            otherConcepts.unshift(primaryConcept);
        }

        return otherConcepts;
    }

    static lookupAction(actionName, lookupConcepts = [], primitiveOptions = {}) {
        let mark = VarvPerformance.start();

        //Filter null and undefined
        lookupConcepts = lookupConcepts.filter((concept)=>{
            return concept != null;
        });

        //Make unique
        lookupConcepts = new Set(lookupConcepts);

        //Add other concepts
        for(let concept of VarvEngine.conceptTypeMap.values()) {
            lookupConcepts.add(concept);
        }

        if(VarvEngine.DEBUG) {
            console.groupCollapsed("Looking up:", actionName, [...lookupConcepts].map((concept)=>{return concept.name}));
        }

        let action = VarvEngine.lookupActionInternal(actionName, lookupConcepts, primitiveOptions);

        if(VarvEngine.DEBUG) {
            if(action != null) {
                console.log("Found action:", action);
            }
            console.groupEnd();
        }

        VarvPerformance.stop("VarvEngine.lookupAction", mark, {actionName});

        return action;
    }

    /**
     * @private
     * @param {string} actionName
     * @param {Set<Concept>} lookupConcepts
     * @returns {Action|null}
     */
    static lookupActionInternal(actionName, lookupConcepts, primitiveOptions = {}) {

        let conceptName = null;

        let split = actionName.split(".");

        if(split.length === 1) {
            actionName = split[0];
        } else if(split.length === 2) {
            conceptName = split[0];
            actionName = split[1];
        } else {
            throw new Error("Only able to lookup actions of the form 'actionname' or 'concept.actionname'");
        }

        if(VarvEngine.DEBUG) {
            console.log("Checking if lookup was on the form 'concept.actionname'");
        }

        if(conceptName != null) {
            if(VarvEngine.DEBUG) {
                console.log("Lookup was 'concept.actionname', forcing lookup to specific concept: ", conceptName);
            }

            let concept = this.conceptTypeMap.get(conceptName);

            if (concept != null) {
                if(VarvEngine.DEBUG) {
                    console.log("Trying to lookup action [" + actionName + "] on concept [" + concept.name + "]");
                }
                return concept.getAction(actionName);
            } else {
                throw new Error("Attempted lookup on form 'concept.actionname' where concept did not exist!");
            }
        }

        //Lookup action directly from name, trying default concept first
        if(lookupConcepts!=null && lookupConcepts.size > 0) {
            if(VarvEngine.DEBUG) {
                console.group("Trying to lookup on lookupConcepts in order");
            }
            for(let lookupConcept of lookupConcepts) {
                if(VarvEngine.DEBUG) {
                    console.log("Trying lookup on concept:", lookupConcept.name);
                }

                let action = lookupConcept.getAction(actionName);

                if(action != null) {
                    if(VarvEngine.DEBUG) {
                        console.groupEnd();
                    }
                    return action;
                }
            }
            if(VarvEngine.DEBUG) {
                console.groupEnd();
            }
        }

        if(VarvEngine.DEBUG) {
            console.log("Trying lookup on primitive actions...")
        }

        //Try primitive actions?
        try {
            if (Action.hasPrimitiveAction(actionName)) {
                return Action.getPrimitiveAction(actionName, primitiveOptions, null);
            }
        } catch(e) {}

        if(VarvEngine.DEBUG) {
            console.log("Action not found!");
        }

        return null;
    }

    static isKnownConceptType(type) {
        return VarvEngine.conceptTypeMap.has(type);
    }

    static registerConceptFromUUID(uuid, concept) {
        let mark = VarvPerformance.start();
        //Legacy register of all direct datastores
        for(let ds of Datastore.getAllDatastores()) {
            if(ds instanceof DirectDatastore) {
                ds.registerConceptFromUUID(uuid, concept);
            }
        }

        //Precache ourselves aswell
        VarvEngine.conceptUUIDCache.set(uuid, concept);

        VarvPerformance.stop("VarvEngine.registerConceptFromUUID", mark);
    }

    static deregisterConceptFromUUID(uuid, concept) {
        this.conceptUUIDCache.delete(uuid);

        //Also clear lookupTarget cache for this concept, if the concept was the target
        let possibleCache = VarvEngine.lookupTargetCache.get(concept.name);
        if(possibleCache?.data === uuid) {
            VarvEngine.lookupTargetCache.delete(concept.name);
        }

        //Legacy register of all direct datastores
        for(let ds of Datastore.getAllDatastores()) {
            if(ds instanceof DirectDatastore) {
                ds.deregisterConceptFromUUID(uuid);
            }
        }
    }

    static registerConceptFromType(type, concept) {
        let oldConcept = this.conceptTypeMap.get(type);

        if(oldConcept != null && oldConcept != concept) {
            console.warn("Registering ["+type+"] already registered", oldConcept, concept);
        }

        VarvEngine.conceptTypeMap.set(type, concept);
    }

    static deregisterConceptFromType(type) {
        for(let ds of Datastore.getAllDatastores()) {
            if(ds instanceof DirectDatastore) {
                ds.deregisterConceptFromType(type);
            }
        }

        VarvEngine.conceptTypeMap.delete(type);
    }

    /**
     * Legacy start, no longer used, Varv starts by itself
     */
    static async start() {
        console.log("await VarvEngine.start() legacy function no longer does anything, Varv is always started - you can remove this call from your code");
    }
    
    static async init(){
        let reloading = false;
        let reloadQueueId = null;

        let foundDefinitionFragments = [];

        const live = new LiveElement("code-fragment[data-type='text/varv'],code-fragment[data-type='text/varvscript']");

        let firstRun = true;

        let fragmentChangedHandler = (fragment)=>{
            if(fragment.fragment != null) {
                fragment = fragment.fragment;
            }
            if(fragment.auto) {
                queueReload();
            }
        }

        let fragmentAutoChangedHandler = ()=>{
            queueReload();
        }

        live.forEach((elm)=>{
            let fragment = Fragment.one(elm);
            elm.fragmentLink = fragment;

            foundDefinitionFragments.push(fragment);

            fragment.registerOnFragmentChangedHandler(fragmentChangedHandler);
            fragment.registerOnAutoChangedHandler(fragmentAutoChangedHandler);

            if(!firstRun) {
                if(fragment.auto) {
                    queueReload();
                }
            }
        });

        live.removed((elm)=>{
            let fragment = elm.fragmentLink;
            foundDefinitionFragments.splice(foundDefinitionFragments.indexOf(fragment), 1);
            fragment.unRegisterOnFragmentChangedHandler(fragmentChangedHandler);
            fragment.unRegisterOnAutoChangedHandler(fragmentAutoChangedHandler);
            if(elm.hasAttribute("auto")) {
                queueReload();
            }
        });

        firstRun = false;
        await queueReload(true);

        async function reload() {
            let mark = VarvPerformance.start();
            reloading = true;
            if (VarvEngine.DEBUG) {
                console.group("Reloading VarvEngine....");
            }

            if (VarvEngine.DEBUG) {
                console.log("Destroying old engine...");
            }
            for (let concept of VarvEngine.concepts) {
                await concept.destroy();
            }

            VarvEngine.concepts = [];

            for (let datastore of Datastore.datastores.values()) {
                if (VarvEngine.DEBUG) {
                    console.log("Destroying datastore:", datastore);
                }
                datastore.destroy();
            }
            Datastore.datastores.clear();

            VarvEngine.conceptUUIDCache.clear();
            VarvEngine.conceptTypeMap.clear();
            VarvEngine.lookupPropertyCache.clear();
            VarvEngine.lookupTargetCache.clear();
            PropertyCache.reset();

            if (VarvEngine.DEBUG) {
                console.log("Merging definition fragments...");
            }

            let combinedObj = {};

            for (let fragment of foundDefinitionFragments) {
                if (fragment.auto) {
                    let varvJson = await fragment.require();

                    combinedObj = VarvEngine.merge(combinedObj, varvJson);

                    //Attempt to merge conflicts we know how to handle:
                    VarvEngine.resolveMergeConflicts(combinedObj);
                } else {
                    if (VarvEngine.DEBUG) {
                        console.log("Skipping disabled fragment:", fragment);
                    }
                }
            }

            if (VarvEngine.DEBUG) {
                console.log("Combined Spec:", combinedObj);
            }

            if (VarvEngine.DEBUG) {
                console.log("Loading new engine...");
            }

            let spec = ConceptLoader.parseSpec(combinedObj);

            VarvEngine.concepts = await ConceptLoader.loadSpec(spec);

            VarvEngine.concepts.forEach((concept)=>{
                concept.doAfterSpecLoadSetup(ConceptLoader.DEBUG);
            });

            await VarvEngine.sendEvent("engineReloaded", VarvEngine.concepts);

            if (VarvEngine.DEBUG) {
                console.log("Reload complete...", VarvEngine.concepts);
                console.groupEnd();
            }
            reloading = false;
            VarvPerformance.stop("VarvEngine.reload", mark);
        }

        function queueReload(initial=false) {
            return new Promise((resolve)=>{
                if(VarvEngine.DEBUG) {
                    console.log("Queuing reload...");
                }

                if(reloadQueueId != null) {
                    window.clearTimeout(reloadQueueId);
                }

                reloadQueueId = setTimeout(()=>{
                    reloadQueueId = null;
                    if(!reloading) {
                        reloading = true;
                        reload().then(()=>{
                            if (!initial){
                                iziToast.success({
                                    title: '',
                                    message: 'Successfully loaded Varv!',
                                    transitionIn: "fadeIn",
                                    transitionOut: 'fadeOut',
                                    position: "topCenter",
                                    timeout: 2000,
                                    close: false,
                                    closeOnClick: true
                                });
                            }
                            reloading = false;
                            resolve();
                        }).catch((e)=>{
                            iziToast.error({
                                title: '',
                                message: 'Error reloading Varv: '+e.message,
                                transitionIn: "fadeIn",
                                transitionOut: 'fadeOut',
                                position: "topCenter",
                                timeout: 2000,
                                close: false,
                                closeOnClick: true
                            });
                            console.groupEnd();
                            console.error(e);
                            reloading = false;
                            resolve();
                        });
                    } else {
                        queueReload().then(()=>{
                            resolve();
                        });
                    }
                }, initial?0:RELOAD_TIMEOUT);
            });
        }

        EventSystem.registerEventCallback("Varv.Restart", ()=>{
            queueReload();
        });
    }

    static async lookupTarget(concept, useOtherConcepts=true) {
        let target = null;

        let mark = VarvPerformance.start();

        let cache = VarvEngine.lookupTargetCache.get(concept.name);
        if(cache != null) {
            VarvPerformance.stop("VarvEngine.lookupTarget.cached", mark);
            cache.hit++;
            return cache.data;
        }

        let instanceTypes = [];
        instanceTypes.push(concept.name);

        if(useOtherConcepts) {
            instanceTypes.push(...VarvEngine.getAllImplementingConceptNames(concept.name));
        }

        let uuids = await VarvEngine.lookupInstances(instanceTypes, null, null,2, null);
        if(uuids.length > 0) {
            target = uuids[0];
            if(uuids.length > 1) {
                console.warn("[lookupTarget] Multiple uuid's exist for concept ["+concept.name+"]", uuids);
            }

            VarvPerformance.stop("VarvEngine.lookupTarget", mark);

            VarvEngine.lookupTargetCache.set(concept.name, {
                data: target,
                hit: 0
            });

            return target;
        }

        throw new Error("No instance of concept found: "+concept.name);
    }

    /**
     * The object returned from lookupProperty
     * @typedef {object} lookupPropertyObject
     * @property {Concept} concept The concept the property was found on
     * @property {Property} property The property itself
     * @property {string} target The target to look up the property on
     */

    /**
     *
     * @param {string|Concept} contextTarget
     * @param {Concept} localConcept
     * @param {string} propertyName
     * @returns {null|lookupPropertyObject}
     */
    static async lookupProperty(contextTarget, localConcept, propertyName) {
        const DEBUG_LOOKUP_PROPERTY = false;

        const mark = VarvPerformance.start();

        const lookupPropertyCacheKey = "CTX:"+(contextTarget?.target!=null?contextTarget.target:"NULL")+"CPT:"+(localConcept!=null?localConcept.name:"NULL")+"PN:"+propertyName;

        let cache = VarvEngine.lookupPropertyCache.get(lookupPropertyCacheKey);

        if(cache != null) {
            if(cache.isValid()) {
                cache.hit++;
                VarvPerformance.stop("VarvEngine.lookupProperty.cached", mark);
                return cache.data;
            } else {
                VarvEngine.lookupPropertyCache.delete(lookupPropertyCacheKey);
            }
        }

        if(VarvEngine.DEBUG || DEBUG_LOOKUP_PROPERTY) {
            console.groupCollapsed("Looking up property:", propertyName, contextTarget, localConcept);
            console.trace();
        }

        //Lookup of form concept.property, overrides all the other lookup types
        let conceptName = null;

        let split = propertyName.split(".");

        if(split.length === 1) {
            propertyName = split[0];
        } else if(split.length === 2) {
            conceptName = split[0];
            propertyName = split[1];
        } else {
            throw new Error("Only able to lookup actions of the form 'actionname' or 'concept.actionname'");
        }

        if (conceptName != null) {
            if (VarvEngine.DEBUG || DEBUG_LOOKUP_PROPERTY) {
                console.log("Lookup of form concept.property...", conceptName, propertyName);
            }

            let lookupConcept = VarvEngine.getConceptFromType(conceptName);

            if (conceptName === "lastTarget") {
                console.warn("Should never see this (conceptName === 'lastTarget') ?????");
            }

            if (lookupConcept != null) {
                let lookupProperty = lookupConcept.getProperty(propertyName);

                if (lookupProperty != null) {

                    let lookupTarget = null;

                    try {
                        if (contextTarget != null && (await VarvEngine.getConceptFromUUID(contextTarget)).isA(conceptName)) {
                            //The current target was of this concept, lets assume that is the wanted target?
                            lookupTarget = contextTarget;
                        } else {
                            lookupTarget = await VarvEngine.lookupTarget(lookupConcept);
                        }
                    } catch(e) {
                        //Ignore for now?
                    }

                    if (VarvEngine.DEBUG || DEBUG_LOOKUP_PROPERTY) {
                        console.log("Found concept.property, ", lookupProperty, lookupConcept, lookupTarget);
                        console.groupEnd();
                    }

                    VarvPerformance.stop("VarvEngine.lookupProperty.concept.property", mark);

                    let result = {
                        property: lookupProperty,
                        concept: lookupConcept,
                        target: lookupTarget,
                        type: "specificConcept"
                    }

                    VarvEngine.lookupPropertyCache.set(lookupPropertyCacheKey, {
                        isValid: () => {
                            return VarvEngine.lookupTargetCache.get(lookupConcept.name) != null;
                        },
                        hit: 0,
                        data: result
                    });

                    return result;
                } else {
                    throw new Error("Lookup property on [" + split.join(".") + "], property does not exist: " + propertyName);
                }
            } else {
                throw new Error("Lookup property on [" + split.join(".") + "], concept does not exist: " + conceptName);
            }
        }

        //Lookup on contextConcept
        try {
            let contextConcept = null;

            if(contextTarget instanceof Concept) {
                contextConcept = contextTarget;
                contextTarget = null;
            } else {
                let promiseOrValue = VarvEngine.getConceptFromUUID(contextTarget);
                if(promiseOrValue instanceof Promise) {
                    promiseOrValue = await promiseOrValue;
                }
                contextConcept = promiseOrValue;
            }

            let property = contextConcept.getProperty(propertyName);

            if (VarvEngine.DEBUG || DEBUG_LOOKUP_PROPERTY) {
                console.log("Found on contextConcept", contextConcept.name);
                console.groupEnd();
            }

            VarvPerformance.stop("VarvEngine.lookupProperty.contextConcept", mark);

            return {
                property: property,
                concept: contextConcept,
                target: contextTarget,
                type: "contextConcept"
            }
        } catch(e) {
            //Ignore
        }

        //Lookup on localConcept
        try {
            let property = localConcept.getProperty(propertyName);

            if(VarvEngine.DEBUG || DEBUG_LOOKUP_PROPERTY) {
                console.log("Found on localConcept", localConcept.name);
                console.groupEnd();
            }

            VarvPerformance.stop("VarvEngine.lookupProperty.localConcept", mark);

            let target = null;
            try {
                target = await VarvEngine.lookupTarget(localConcept)
            } catch(e) {
                //Ignore
            }

            return {
                property: property,
                concept: localConcept,
                target: target,
                type: "localConcept"
            }
        } catch(e) {
            //Ignore
        }

        //Lookup on globalConcept
        for(let globalConcept of VarvEngine.concepts) {
            try {
                let property = globalConcept.getProperty(propertyName);

                if(VarvEngine.DEBUG || DEBUG_LOOKUP_PROPERTY) {
                    console.log("Found on globalConcept", globalConcept.name);
                    console.groupEnd();
                }

                VarvPerformance.stop("VarvEngine.lookupProperty.globalConcept", mark);

                let target = null;
                try {
                    target = await VarvEngine.lookupTarget(globalConcept)
                } catch(e) {
                    //Ignore
                }

                return {
                    property: property,
                    concept: globalConcept,
                    target: target,
                    type: "globalConcept"
                }
            } catch(e) {
                //Ignore
            }
        }

        VarvPerformance.stop("VarvEngine.lookupProperty.notFound", mark);

        if(VarvEngine.DEBUG || DEBUG_LOOKUP_PROPERTY) {
            console.log("Not found...");
            console.groupEnd();
        }

        return null;
    }

    /**
     * Lookup an unknown reference
     * @param {string} reference - The reference to lookup
     * @param {Concept} localConcept=null - The concept to lookup properties on first, before trying globally
     * @param {boolean} executeAtOnce - If true, the value of the function is returned, and not the function.
     * @returns {function|object|string} - An object containing the type of reference if known, or just the reference if still unknown
     */
    static lookupReference(reference, lookupConcepts = []) {
        const mark = VarvPerformance.start();

        let allConcepts = new Set();
        if(lookupConcepts != null) {
            if(!Array.isArray(lookupConcepts)) {
                lookupConcepts = [lookupConcepts];
            }

            lookupConcepts.forEach((concept)=>{
                allConcepts.add(concept);
            });
        }

        VarvEngine.concepts.forEach((concept)=>{
            allConcepts.add(concept);
        });

        let lookup = VarvEngine.lookupReferenceInternal(reference, allConcepts);

        VarvPerformance.stop("VarvEngine.lookupReference", mark, {reference});

        return lookup;
    }

    static lookupReferenceInternal(reference, lookupConcepts) {
        if(VarvEngine.DEBUG) {
            console.groupCollapsed("Looking up unknown reference:", reference);
        }

        //Check for concept type
        if (VarvEngine.getConceptFromType(reference) != null) {
            if(VarvEngine.DEBUG) {
                console.log("Was concept!");
                console.groupEnd();
            }
            return {
                "concept": reference
            }
        }

        //Check for view
        if (DOMView?.singleton.existsAsViewElement(reference)) {
            if(VarvEngine.DEBUG) {
                console.log("Was view!");
                console.groupEnd();
            }
            return {
                "view": reference
            }
        }

        //Check for global property
        for(let concept of lookupConcepts) {
            try {
                concept.getProperty(reference);

                if(VarvEngine.DEBUG) {
                    console.log("Was property on concept: [" + concept.name + "]");
                    console.groupEnd();
                }

                return {
                    "concept": concept.name,
                    "property": reference
                }
            } catch(e) {
                //Ignore, just means we had no property of given name
            }
        }

        if(VarvEngine.DEBUG) {
            console.log("Was unknown!");
            console.groupEnd();
        }

        return {
            "unknown": reference
        };
    }

    static resolveMergeConflicts(spec) {
        const DEBUG_MERGE = VarvEngine.DEBUG || false;

        function replaceInSpec(prefix, newValue) {
            if(prefix.startsWith("spec.")) {
                prefix = prefix.substring(5);
            }

            let currentPath = spec;
            let lastPath = null;
            let lastPrefixPart = null;
            prefix.split(".").forEach((prefixPart)=>{
                lastPath = currentPath;
                lastPrefixPart = prefixPart;

                if(prefixPart.indexOf("[") !== -1) {
                    let key = prefixPart.substring(0, prefixPart.indexOf("["));
                    let index = prefixPart.substring(prefixPart.indexOf("[")+1, prefixPart.indexOf("]"));
                    //Array entry
                    currentPath = currentPath[key][index];
                } else {
                    //Object entry
                    currentPath = currentPath[prefixPart];
                }
            });

            if(lastPath != null) {
                if(lastPrefixPart.indexOf("[") !== -1) {
                    let key = lastPrefixPart.substring(0, lastPrefixPart.indexOf("["));
                    let index = lastPrefixPart.substring(lastPrefixPart.indexOf("[")+1, lastPrefixPart.indexOf("]"));
                    //Array entry
                    lastPath[key][index] = newValue;
                } else {
                    //Object entry
                    lastPath[lastPrefixPart] = newValue;
                }

                return true;
            }

            return false;
        }

        function handleMergeConflict(prefix, json) {
            let merged = false;
            if(DEBUG_MERGE) {
                console.group("Attempting to merge: ", prefix, json.mergeConflict);
            }

            if(json.mergeConflict.type === "arrayMerge") {
                if(DEBUG_MERGE) {
                    console.log("ArrayMerge");
                }

                if(prefix.endsWith(".enum")) {
                    if(DEBUG_MERGE) {
                        console.log("Enum merge!")
                    }

                    merged = replaceInSpec(prefix, json.mergeConflict.part2);
                } else if(prefix.endsWith(".then")) {
                    if(DEBUG_MERGE) {
                        console.log("Action then merge!");
                    }

                    merged = replaceInSpec(prefix, json.mergeConflict.part2);
                } else if(prefix.endsWith(".when")) {
                    if(DEBUG_MERGE) {
                        console.log("Action when merge!");
                    }

                    merged = replaceInSpec(prefix, json.mergeConflict.part2);
                } else if(prefix.match(/actions\.\S+$/)) {
                    if(DEBUG_MERGE) {
                        console.log("Action array merge!");
                    }

                    merged = replaceInSpec(prefix, {"then": json.mergeConflict.part2});
                } else if(prefix.endsWith(".extensions")) {
                    let result = [];
                    json.mergeConflict.part1.forEach((ext)=>{
                        result.push(ext);
                    });
                    json.mergeConflict.part2.forEach((ext)=>{
                        result.push(ext);
                    });

                    merged = replaceInSpec(prefix, result);
                }
            } else if(json.mergeConflict.type == null) {
                if(DEBUG_MERGE) {
                    console.log("Unknown merge type!");
                }

                if(prefix.match(/actions\.\S+$/)) {
                    let then = null;
                    let when = null;

                    if(Array.isArray(json.mergeConflict.part1)) {
                        then = json.mergeConflict.part1;
                        when = json.mergeConflict.part2.when;
                    } else if(Array.isArray(json.mergeConflict.part2)) {
                        then = json.mergeConflict.part2;
                        when = json.mergeConflict.part1.when;
                    } else {
                        //Unknown action merge
                    }

                    merged = replaceInSpec(prefix, {then, when});
                }
            }

            if(DEBUG_MERGE) {
                console.groupEnd();
            }
            return merged;
        }

        //Check for any mergeconflicts left...
        function findMergeConflict(json, prefix="") {
            if(typeof json === "object") {
                if(Array.isArray(json)) {
                    json.forEach((elm, index)=>{
                        findMergeConflict(elm, prefix+"["+index+"]");
                    });
                } else if(json != null) {
                    if(json.mergeConflict != null) {
                        let handled = handleMergeConflict(prefix, json);
                        if(!handled) {
                            console.warn("Found unhandled mergeConflict:", prefix, json);
                        }
                    }

                    Object.keys(json).forEach((objKey)=>{
                        let objValue = json[objKey];
                        findMergeConflict(objValue, prefix+"."+objKey);
                    });
                }
            }
        }

        findMergeConflict(spec, "spec");
    }

    static merge(json1, json2) {
        if(VarvEngine.DEBUG) {
            console.groupCollapsed("Merging:", json1, json2);
        }

        if(json1 == null && json2 != null) {
            if(VarvEngine.DEBUG) {
                console.log("Shortcut json2:", json2);
                console.groupEnd();
            }
            return json2;
        } else if(json2 == null && json1 != null) {
            if(VarvEngine.DEBUG) {
                console.log("Shortcut json1:", json1);
                console.groupEnd();
            }
            return json1;
        }

        if(json1 == null && json2 == null) {
            if(VarvEngine.DEBUG) {
                console.log("Null shortcut...");
                console.groupEnd();
            }
            return null;
        }

        //Neither json is null, as that would have resulted in a shortcut...

        let type1 = typeof json1;
        let type2 = typeof json2;

        if(type1 !== type2) {
            //Types were different, but none of the json was null, this should be an error of some sort?
            if(VarvEngine.DEBUG) {
                console.warn("Unable to merge different types:", json1, json2);
                console.groupEnd();
            }
            return {
                "mergeConflict": {
                    "part1": json1,
                    "part2": json2
                }
            };
        }

        let isArray1 = Array.isArray(json1);
        let isArray2 = Array.isArray(json2);

        if(isArray1 != isArray2) {
            if(VarvEngine.DEBUG) {
                console.warn("One was array, other was not!");
                console.groupEnd();
            }
            return {
                "mergeConflict": {
                    "part1": json1,
                    "part2": json2
                }
            };
        }

        if(isArray1) {
            if(VarvEngine.DEBUG) {
                console.log("Array merge...");
            }

            if(VarvEngine.DEBUG) {
                console.groupEnd();
            }

            return {
                "mergeConflict": {
                    "type": "arrayMerge",
                    "part1": json1,
                    "part2": json2
                }
            };
        } else if(type1 === "object") {
            if(VarvEngine.DEBUG) {
                console.log("Object merge...");
            }

            let result = {};

            for(let key of Object.keys(json1)) {
                result[key] = VarvEngine.merge(json1[key], json2[key]);
            }

            for(let key of Object.keys(json2)) {
                if(!result.hasOwnProperty(key)) {
                    //This was not handled already from json1 keys
                    result[key] = VarvEngine.merge(json1[key], json2[key]);
                }
            }

            if(VarvEngine.DEBUG) {
                console.groupEnd();
            }
            return result;
        } else {
            if(VarvEngine.DEBUG) {
                console.log("Value merge...");
            }
            if(json1 !== json2) {
                if(VarvEngine.DEBUG) {
                    console.warn("Non equal values! (Overriding from json2)");
                }
            }

            if(VarvEngine.DEBUG) {
                console.groupEnd();
            }
            return json2;
        }
    }

    static async sendEvent(eventName, detail) {
        if(Datastore.DEBUG) {
            console.group("Sending varv engine event:", eventName, detail);
        }
        let mark = VarvPerformance.start();
        await EventSystem.triggerEventAsync(VarvEngine.VarvEngineEventPrefix+eventName, detail);
        VarvPerformance.stop("VarvEngine.sendEvent."+eventName, mark, detail);
        if(Datastore.DEBUG) {
            console.groupEnd();
        }
    }

    static registerEventCallback(eventName, callback) {
        return EventSystem.registerEventCallback(VarvEngine.VarvEngineEventPrefix+eventName, async (evt)=>{
            await callback(evt.detail);
        });
    }

    static handlePossibleAsync(possiblePromise, callback) {
        if(possiblePromise instanceof Promise) {
            //Was promise, so wait for actual value
            possiblePromise.then((actualValue)=>{
                callback(actualValue);
            });
        } else {
            //No promise, so was actual value
            callback(possiblePromise);
        }
    }
}

VarvEngine.concepts = [];
VarvEngine.DEBUG = false;
VarvEngine.VarvEngineEventPrefix = "VarvEngineEvent.";
VarvEngine.conceptTypeMap = new Map();
VarvEngine.conceptUUIDCache = new Map();
VarvEngine.lookupPropertyCache = new Map();
VarvEngine.lookupTargetCache = new Map();

window.VarvEngine = VarvEngine;

const urlParams = new URLSearchParams(location.search);
window.VarvEngine.disabled = (urlParams.get("varv") === "false");

if(!window.VarvEngine.disabled) {
    wpm.onAllInstalled(async ()=>{
        if(typeof Fragment !== "undefined") {
            Fragment.addAllFragmentsLoadedCallback(async ()=>{
                console.log("Starting VarvEngine with Codestrates");
                await VarvEngine.init();
            });
        } else {
            console.log("Starting VarvEngine");
            await VarvEngine.init();
        };
    });
} else {
    console.log("Varv disabled!");
}