"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var property_util_1 = require("./property.util");
var resources_1 = require("../resources/resources");
var util_1 = require("../helpers/util");
var resource_set_1 = require("../resources/resource-set");
var AJV = require("ajv");
var _ = require("lodash");
var ajv = new AJV({ jsonPointers: true });
var ReferencePropertyImpl = /** @class */ (function () {
    function ReferencePropertyImpl(innerSchema, innerTargetSchema, key, name, idBased, findFunction, addFunction, getFunction) {
        this.innerSchema = innerSchema;
        this.innerTargetSchema = innerTargetSchema;
        this.key = key;
        this.name = name;
        this.idBased = idBased;
        this.findFunction = findFunction;
        this.addFunction = addFunction;
        this.getFunction = getFunction;
    }
    Object.defineProperty(ReferencePropertyImpl.prototype, "label", {
        get: function () {
            return _.find([
                this.innerSchema.title,
                this.name,
                this.innerSchema.$id,
                this.key
            ], function (n) { return !_.isEmpty(n); });
        },
        enumerable: true,
        configurable: true
    });
    Object.defineProperty(ReferencePropertyImpl.prototype, "schema", {
        get: function () {
            return this.innerSchema;
        },
        enumerable: true,
        configurable: true
    });
    Object.defineProperty(ReferencePropertyImpl.prototype, "property", {
        get: function () {
            return this.key;
        },
        enumerable: true,
        configurable: true
    });
    Object.defineProperty(ReferencePropertyImpl.prototype, "targetSchema", {
        get: function () {
            return this.innerTargetSchema;
        },
        enumerable: true,
        configurable: true
    });
    ReferencePropertyImpl.prototype.addToData = function (data, valueToAdd) {
        this.addFunction(data, valueToAdd);
    };
    ReferencePropertyImpl.prototype.getData = function (data) {
        return this.getFunction(data);
    };
    ReferencePropertyImpl.prototype.isIdBased = function () {
        return this.idBased;
    };
    ReferencePropertyImpl.prototype.findReferenceTargets = function (data) {
        return this.findFunction(data);
    };
    return ReferencePropertyImpl;
}());
exports.ReferencePropertyImpl = ReferencePropertyImpl;
/**
 * Validates the given data against the given JSON Schema.
 *
 * @return true if the data adheres to the schema, false otherwise
 */
var checkData = function (data, targetSchema) {
    // use AJV to validate data against targetSchema and return result
    var valid = ajv.validate(targetSchema, data);
    if (_.isBoolean(valid)) {
        return valid;
    }
    return false;
};
var addReference = function (schema, identifyingProperty, propName) {
    return function (data, toAdd) {
        var refValue = toAdd[identifyingProperty];
        if (schema.properties[propName].type === 'array') {
            if (!data[propName]) {
                data[propName] = [];
            }
            data[propName].push(refValue);
        }
        else {
            data[propName] = refValue;
        }
    };
};
/**
 * Retrieves the data that contains the reference targets by resolving the given href uri.
 * Thereby, the root data is gathered based on the protocol and then resolved
 * against the uri's path.
 *
 * Important: This method does not resolve the actual reference targets but only the
 * root data containing them.
 *
 * @return the resolved data containing the reference targets; {null} if the href cannot be resolved
 */
var getReferenceTargetData = function (data, href) {
    var rootData;
    var localTemplatePath;
    if (_.startsWith(href, resource_set_1.RS_PROTOCOL)) {
        var resourceName = href.substring(resource_set_1.RS_PROTOCOL.length).split('/')[0];
        localTemplatePath = href.substring(resource_set_1.RS_PROTOCOL.length + resourceName.length + 1);
        rootData = resources_1.Resources.resourceSet.getResource(resourceName);
        // reference to data in resource set
    }
    else if (_.startsWith(href, 'http://')) {
        // remote data
        console.warn("Remote data resolution is not yet implemented for data links.");
        return null;
    }
    else if (_.startsWith(href, '#') || (href.match(/\{.*\}/) !== null)) {
        // local data
        rootData = data;
        localTemplatePath = href;
    }
    else {
        console.error("'" + href + "' is not a supported URI to specify reference targets in a link block.");
        return {};
    }
    var localPath = localTemplatePath.split(/\/\{.*\}/)[0];
    if (localPath.match(/\{.*\}/) !== null) {
        // the local path only contains the template variable
        return rootData;
    }
    return util_1.resolveLocalData(rootData, localPath);
};
// reference resolvement for id based references
exports.resolveRef = function (schema, findTargets, propName) { return function (data) {
    if (_.isEmpty(data)) {
        return {};
    }
    // get all objects that could be referenced.
    var candidates = findTargets(data);
    var result = {};
    if (_.isEmpty(schema.properties[propName].type)) {
        throw Error("The schema of the property '" + propName + "' does not specify a schema type.");
    }
    if (schema.properties[propName].type === 'array') {
        var ids = data[propName];
        // check that there is at most one reference target for every id
        for (var _i = 0, ids_1 = ids; _i < ids_1.length; _i++) {
            var id = ids_1[_i];
            var idResult = candidates[id];
            if (idResult === undefined) {
                console.warn("Could not resolve the referenced data for id '" + id + "'.");
                continue;
            }
            result[id] = idResult;
        }
    }
    else {
        // use identifying property to identify the referenced object
        var id = data[propName];
        var idResult = candidates[id];
        if (idResult === undefined) {
            console.warn("Could not resolve the referenced data for id '" + id + "'.");
            return result;
        }
        result[id] = idResult;
    }
    return result;
}; };
exports.getFindReferenceTargetsFunction = function (href, schemaId, idProp, modelMapping) {
    return function (data) {
        var candidates = getReferenceTargetData(data, href);
        if (!_.isEmpty(candidates)) {
            var result = {};
            for (var _i = 0, _a = filterObjectsByType(candidates, schemaId, modelMapping); _i < _a.length; _i++) {
                var candidate = _a[_i];
                var id = candidate[idProp];
                result[id] = candidate;
            }
            return result;
        }
        return {};
    };
};
/**
 * Recursive method to find all paths to valid reference targets based on the
 * target schema identifying them.
 * This method searches the target based on the given data and calculates the path
 * based on the current location and adding the next path segment for child properties
 * or array elements.
 *
 * The result is an object mapping from the found paths to the target data object
 * found at the path.
 */
var collectionHelperMap = function (currentPath, data, targetSchema) {
    var result = {};
    if (checkData(data, targetSchema)) {
        // must be done before null check before null might be a valid target
        result[currentPath] = data;
    }
    if (data === undefined || data === null) {
        return result;
    }
    if (Array.isArray(data)) {
        // TODO how to deal with array? index? - assume index for now
        for (var i = 0; i < data.length; i++) {
            var childPath = _.isEmpty(currentPath) ? "" + i : currentPath + "/" + i;
            var childResult = collectionHelperMap(childPath, data[i], targetSchema);
            _.assign(result, childResult);
        }
    }
    else if (!_.isEmpty(data) && typeof data !== 'string') {
        Object.keys(data).forEach(function (key) {
            // NOTE later maybe need to check for refs and thereby circles
            var childPath = _.isEmpty(currentPath) ? key : currentPath + "/" + key;
            var childResult = collectionHelperMap(childPath, data[key], targetSchema);
            _.assign(result, childResult);
        });
    }
    return result;
};
/**
 * Adds the reference path given in toAdd to the data's reference property.
 */
var addPathBasedRef = function (schema, propName) {
    return function (data, toAdd) {
        if (typeof toAdd !== 'string') {
            console.error("Path based reference values must be of type string. The given value was of"
                + (" type '" + typeof toAdd + "' and could not be added."));
            return;
        }
        // const refValue = toAdd[identifyingProperty];
        if (schema.properties[propName].type === 'array') {
            if (!data[propName]) {
                data[propName] = [];
            }
            data[propName].push(toAdd);
        }
        else {
            data[propName] = toAdd;
        }
    };
};
var resolvePathBasedRef = function (href, pathProperty) { return function (data) {
    var targetData = getReferenceTargetData(data, href);
    var paths = data[pathProperty];
    var result = {};
    if (Array.isArray(paths)) {
        for (var _i = 0, paths_1 = paths; _i < paths_1.length; _i++) {
            var path = paths_1[_i];
            var curRes = util_1.resolveLocalData(targetData, path);
            result[path] = curRes;
        }
    }
    else {
        result[paths] = util_1.resolveLocalData(targetData, paths);
    }
    return result;
}; };
var getPathBasedRefTargets = function (href, targetSchema) { return function (data) {
    var targetData = getReferenceTargetData(data, href);
    if (href.indexOf('#') > -1) {
        return collectionHelperMap('', targetData, targetSchema);
    }
    return collectionHelperMap('#', targetData, targetSchema);
}; };
/**
 * Uses the model mapping to filter all objects that are associated with the type
 * defined by the given schema id. If there is no applicable mapping,
 * we assume that no mapping is necessary and do not filter out affected data objects.
 *
 * @param objects the list of data objects to filter
 * @param schemaId The id of the JsonSchema defining the type to filter for
 * @return The filtered data objects or all objects if there is no applicable mapping
 */
var filterObjectsByType = function (objects, schemaId, modelMapping) {
    // No filtering possible without a mapping, return all
    if (modelMapping === undefined) {
        return objects;
    }
    return objects.filter(function (value) {
        var valueSchemaId = getSchemaIdForObject(value, modelMapping);
        if (valueSchemaId === null) {
            return true;
        }
        return valueSchemaId === schemaId;
    });
};
/**
 * Uses the model mapping to find the schema id defining the type of the given object.
 * If no schema id can be determined either because the object is empty, there is no model
 * mapping, or the object does not contain a mappable property.
 * TODO expected behavior?
 *
 * @param object The object whose type is determined
 * @return The schema id of the object or null if it could not be determined
 */
var getSchemaIdForObject = function (object, modelMapping) {
    if (modelMapping !== undefined && !_.isEmpty(object)) {
        var mappingAttribute = modelMapping.attribute;
        if (!_.isEmpty(mappingAttribute)) {
            var mappingValue = object[mappingAttribute];
            var schemaElementId = modelMapping.mapping[mappingValue];
            return !_.isEmpty(schemaElementId) ? schemaElementId : null;
        }
    }
    return null;
};
/**
 * Retrieves an array of reference properties based on the provided schema.
 * @param schema The schema to check for references
 * @return The array of {@link ReferenceProperty} or empty if no references are available
 * @see ReferenceProperty
 */
exports.getReferenceProperties = function (schema, editorContext) {
    if (schema.$ref !== undefined) {
        return exports.getReferenceProperties(property_util_1.makeSelfContainedSchema(editorContext.dataSchema, schema.$ref), editorContext);
    }
    // tslint:disable:no-string-literal
    if (schema['links']) {
        var links = schema['links'];
        // tslint:enable:no-string-literal
        var result_1 = [];
        links.forEach(function (link) {
            if (_.isEmpty(link.targetSchema) || _.isEmpty(link.href)) {
                console.warn("Could not create link property because the configuration was invalid", link);
                return;
            }
            var targetSchema;
            // TODO what if schema is url but not resolved?
            if (link.targetSchema.$ref !== undefined) {
                targetSchema = property_util_1.makeSelfContainedSchema(editorContext.dataSchema, link.targetSchema.$ref);
            }
            else if (link.targetSchema.resource !== undefined) {
                var resSchema = resources_1.Resources.resourceSet.getResource(link.targetSchema.resource);
                if (resSchema === undefined) {
                    console.error("Could not find target schema: There is no resource with name:" +
                        ("" + link.targetSchema.resource));
                    return [];
                }
                targetSchema = resSchema;
            }
            else {
                targetSchema = link.targetSchema;
            }
            var href = link.href;
            var variableWrapped = href.match(/\{.*\}/)[0];
            var variable = variableWrapped.substring(1, variableWrapped.length - 1);
            var findRefTargets;
            var resolveReference;
            var addToReference;
            var idBased;
            // assume targetSchema is resolved
            if (!_.isEmpty(editorContext.identifyingProperty) && !_.isEmpty(targetSchema.$id)
                && !_.isEmpty(targetSchema.properties)
                && !_.isEmpty(targetSchema.properties[editorContext.identifyingProperty])) {
                // use id based referencing & reuse existing code for now
                // TODO reuse of existing id behavior sub-optimal (does not search cascaded)
                idBased = true;
                var schemaId = targetSchema.$id;
                findRefTargets = exports.getFindReferenceTargetsFunction(href, schemaId, editorContext.identifyingProperty, editorContext.modelMapping);
                resolveReference = exports.resolveRef(schema, findRefTargets, variable);
                addToReference = addReference(schema, editorContext.identifyingProperty, variable);
            }
            else {
                // use path based referencing
                idBased = false;
                findRefTargets = getPathBasedRefTargets(href, targetSchema);
                resolveReference = resolvePathBasedRef(href, variable);
                addToReference = addPathBasedRef(schema, variable);
            }
            result_1.push(new ReferencePropertyImpl(schema.properties[variable], targetSchema, variable, variable, idBased, findRefTargets, addToReference, resolveReference));
        });
        return result_1;
    }
    if (schema.anyOf !== undefined) {
        return schema.anyOf.reduce(function (prev, cur) {
            return prev.concat(exports.getReferenceProperties(cur, editorContext));
        }, []);
    }
    return [];
};
//# sourceMappingURL=reference.service.js.map