/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Red Hat, Inc. All rights reserved.
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
'use strict';
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.YAMLSchemaService = exports.FilePatternAssociation = exports.MODIFICATION_ACTIONS = void 0;
const yamlLanguageService_1 = require("../yamlLanguageService");
const jsonSchemaService_1 = require("vscode-json-languageservice/lib/umd/services/jsonSchemaService");
const vscode_uri_1 = require("vscode-uri");
const nls = require("vscode-nls");
const strings_1 = require("../utils/strings");
const yaml_1 = require("yaml");
const path = require("path");
const modelineUtil_1 = require("./modelineUtil");
const ajv_1 = require("ajv");
const schemaUtils_1 = require("../utils/schemaUtils");
const localize = nls.loadMessageBundle();
const ajv = new ajv_1.default();
// load JSON Schema 07 def to validate loaded schemas
// eslint-disable-next-line @typescript-eslint/no-var-requires
const jsonSchema07 = require('ajv/dist/refs/json-schema-draft-07.json');
const schema07Validator = ajv.compile(jsonSchema07);
var MODIFICATION_ACTIONS;
(function (MODIFICATION_ACTIONS) {
    MODIFICATION_ACTIONS[MODIFICATION_ACTIONS["delete"] = 0] = "delete";
    MODIFICATION_ACTIONS[MODIFICATION_ACTIONS["add"] = 1] = "add";
    MODIFICATION_ACTIONS[MODIFICATION_ACTIONS["deleteAll"] = 2] = "deleteAll";
})(MODIFICATION_ACTIONS = exports.MODIFICATION_ACTIONS || (exports.MODIFICATION_ACTIONS = {}));
class FilePatternAssociation {
    constructor(pattern) {
        try {
            this.patternRegExp = new RegExp((0, strings_1.convertSimple2RegExpPattern)(pattern) + '$');
        }
        catch (e) {
            // invalid pattern
            this.patternRegExp = null;
        }
        this.schemas = [];
    }
    addSchema(id) {
        this.schemas.push(id);
    }
    matchesPattern(fileName) {
        return this.patternRegExp && this.patternRegExp.test(fileName);
    }
    getSchemas() {
        return this.schemas;
    }
}
exports.FilePatternAssociation = FilePatternAssociation;
class YAMLSchemaService extends jsonSchemaService_1.JSONSchemaService {
    constructor(requestService, contextService, promiseConstructor) {
        super(requestService, contextService, promiseConstructor);
        this.schemaUriToNameAndDescription = new Map();
        this.customSchemaProvider = undefined;
        this.requestService = requestService;
        this.schemaPriorityMapping = new Map();
    }
    registerCustomSchemaProvider(customSchemaProvider) {
        this.customSchemaProvider = customSchemaProvider;
    }
    getAllSchemas() {
        const result = [];
        const schemaUris = new Set();
        for (const filePattern of this.filePatternAssociations) {
            const schemaUri = filePattern.uris[0];
            if (schemaUris.has(schemaUri)) {
                continue;
            }
            schemaUris.add(schemaUri);
            const schemaHandle = {
                uri: schemaUri,
                fromStore: false,
                usedForCurrentFile: false,
            };
            if (this.schemaUriToNameAndDescription.has(schemaUri)) {
                const { name, description, versions } = this.schemaUriToNameAndDescription.get(schemaUri);
                schemaHandle.name = name;
                schemaHandle.description = description;
                schemaHandle.fromStore = true;
                schemaHandle.versions = versions;
            }
            result.push(schemaHandle);
        }
        return result;
    }
    resolveSchemaContent(schemaToResolve, schemaURL, dependencies) {
        return __awaiter(this, void 0, void 0, function* () {
            const resolveErrors = schemaToResolve.errors.slice(0);
            let schema = schemaToResolve.schema;
            const contextService = this.contextService;
            if (!schema07Validator(schema)) {
                const errs = [];
                for (const err of schema07Validator.errors) {
                    errs.push(`${err.instancePath} : ${err.message}`);
                }
                resolveErrors.push(`Schema '${(0, schemaUtils_1.getSchemaTitle)(schemaToResolve.schema, schemaURL)}' is not valid:\n${errs.join('\n')}`);
            }
            const findSection = (schema, path) => {
                if (!path) {
                    return schema;
                }
                // eslint-disable-next-line @typescript-eslint/no-explicit-any
                let current = schema;
                if (path[0] === '/') {
                    path = path.substr(1);
                }
                path.split('/').some((part) => {
                    current = current[part];
                    return !current;
                });
                return current;
            };
            const merge = (target, sourceRoot, sourceURI, path) => {
                const section = findSection(sourceRoot, path);
                if (section) {
                    for (const key in section) {
                        if (Object.prototype.hasOwnProperty.call(section, key) && !Object.prototype.hasOwnProperty.call(target, key)) {
                            target[key] = section[key];
                        }
                    }
                }
                else {
                    resolveErrors.push(localize('json.schema.invalidref', "$ref '{0}' in '{1}' can not be resolved.", path, sourceURI));
                }
            };
            const resolveExternalLink = (node, uri, linkPath, parentSchemaURL, parentSchemaDependencies
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            ) => {
                if (contextService && !/^\w+:\/\/.*/.test(uri)) {
                    uri = contextService.resolveRelativePath(uri, parentSchemaURL);
                }
                uri = this.normalizeId(uri);
                const referencedHandle = this.getOrAddSchemaHandle(uri);
                return referencedHandle.getUnresolvedSchema().then((unresolvedSchema) => {
                    parentSchemaDependencies[uri] = true;
                    if (unresolvedSchema.errors.length) {
                        const loc = linkPath ? uri + '#' + linkPath : uri;
                        resolveErrors.push(localize('json.schema.problemloadingref', "Problems loading reference '{0}': {1}", loc, unresolvedSchema.errors[0]));
                    }
                    merge(node, unresolvedSchema.schema, uri, linkPath);
                    node.url = uri;
                    // eslint-disable-next-line @typescript-eslint/no-use-before-define
                    return resolveRefs(node, unresolvedSchema.schema, uri, referencedHandle.dependencies);
                });
            };
            const resolveRefs = (node, parentSchema, parentSchemaURL, parentSchemaDependencies
            // eslint-disable-next-line @typescript-eslint/no-explicit-any
            ) => __awaiter(this, void 0, void 0, function* () {
                if (!node || typeof node !== 'object') {
                    return null;
                }
                const toWalk = [node];
                const seen = [];
                // eslint-disable-next-line @typescript-eslint/no-explicit-any
                const openPromises = [];
                const collectEntries = (...entries) => {
                    for (const entry of entries) {
                        if (typeof entry === 'object') {
                            toWalk.push(entry);
                        }
                    }
                };
                const collectMapEntries = (...maps) => {
                    for (const map of maps) {
                        if (typeof map === 'object') {
                            for (const key in map) {
                                const entry = map[key];
                                if (typeof entry === 'object') {
                                    toWalk.push(entry);
                                }
                            }
                        }
                    }
                };
                const collectArrayEntries = (...arrays) => {
                    for (const array of arrays) {
                        if (Array.isArray(array)) {
                            for (const entry of array) {
                                if (typeof entry === 'object') {
                                    toWalk.push(entry);
                                }
                            }
                        }
                    }
                };
                const handleRef = (next) => {
                    const seenRefs = [];
                    while (next.$ref) {
                        const ref = next.$ref;
                        const segments = ref.split('#', 2);
                        //return back removed $ref. We lost info about referenced type without it.
                        next._$ref = next.$ref;
                        delete next.$ref;
                        if (segments[0].length > 0) {
                            openPromises.push(resolveExternalLink(next, segments[0], segments[1], parentSchemaURL, parentSchemaDependencies));
                            return;
                        }
                        else {
                            if (seenRefs.indexOf(ref) === -1) {
                                merge(next, parentSchema, parentSchemaURL, segments[1]); // can set next.$ref again, use seenRefs to avoid circle
                                seenRefs.push(ref);
                            }
                        }
                    }
                    collectEntries(next.items, next.additionalItems, next.additionalProperties, next.not, next.contains, next.propertyNames, next.if, next.then, next.else);
                    collectMapEntries(next.definitions, next.properties, next.patternProperties, next.dependencies);
                    collectArrayEntries(next.anyOf, next.allOf, next.oneOf, next.items, next.schemaSequence);
                };
                if (parentSchemaURL.indexOf('#') > 0) {
                    const segments = parentSchemaURL.split('#', 2);
                    if (segments[0].length > 0 && segments[1].length > 0) {
                        const newSchema = {};
                        yield resolveExternalLink(newSchema, segments[0], segments[1], parentSchemaURL, parentSchemaDependencies);
                        for (const key in schema) {
                            if (key === 'required') {
                                continue;
                            }
                            if (Object.prototype.hasOwnProperty.call(schema, key) && !Object.prototype.hasOwnProperty.call(newSchema, key)) {
                                newSchema[key] = schema[key];
                            }
                        }
                        schema = newSchema;
                    }
                }
                while (toWalk.length) {
                    const next = toWalk.pop();
                    if (seen.indexOf(next) >= 0) {
                        continue;
                    }
                    seen.push(next);
                    handleRef(next);
                }
                return Promise.all(openPromises);
            });
            yield resolveRefs(schema, schema, schemaURL, dependencies);
            return new jsonSchemaService_1.ResolvedSchema(schema, resolveErrors);
        });
    }
    getSchemaForResource(resource, doc) {
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        const resolveSchema = () => {
            const seen = Object.create(null);
            const schemas = [];
            let schemaFromModeline = (0, modelineUtil_1.getSchemaFromModeline)(doc);
            if (schemaFromModeline !== undefined) {
                if (!schemaFromModeline.startsWith('file:') && !schemaFromModeline.startsWith('http')) {
                    // If path contains a fragment and it is left intact, "#" will be
                    // considered part of the filename and converted to "%23" by
                    // path.resolve() -> take it out and add back after path.resolve
                    let appendix = '';
                    if (schemaFromModeline.indexOf('#') > 0) {
                        const segments = schemaFromModeline.split('#', 2);
                        schemaFromModeline = segments[0];
                        appendix = segments[1];
                    }
                    if (!path.isAbsolute(schemaFromModeline)) {
                        const resUri = vscode_uri_1.URI.parse(resource);
                        schemaFromModeline = vscode_uri_1.URI.file(path.resolve(path.parse(resUri.fsPath).dir, schemaFromModeline)).toString();
                    }
                    else {
                        schemaFromModeline = vscode_uri_1.URI.file(schemaFromModeline).toString();
                    }
                    if (appendix.length > 0) {
                        schemaFromModeline += '#' + appendix;
                    }
                }
                this.addSchemaPriority(schemaFromModeline, yamlLanguageService_1.SchemaPriority.Modeline);
                schemas.push(schemaFromModeline);
                seen[schemaFromModeline] = true;
            }
            for (const entry of this.filePatternAssociations) {
                if (entry.matchesPattern(resource)) {
                    for (const schemaId of entry.getURIs()) {
                        if (!seen[schemaId]) {
                            schemas.push(schemaId);
                            seen[schemaId] = true;
                        }
                    }
                }
            }
            /**
             * If this resource matches a schemaID directly then use that schema.
             * This will be used in the case where the yaml language server is being used as a library
             * and clients want to save a schema with a particular ID and also use that schema
             * in language features
             */
            const normalizedResourceID = this.normalizeId(resource);
            if (this.schemasById[normalizedResourceID]) {
                schemas.push(normalizedResourceID);
            }
            if (schemas.length > 0) {
                // Join all schemas with the highest priority.
                const highestPrioSchemas = this.highestPrioritySchemas(schemas);
                const schemaHandle = super.createCombinedSchema(resource, highestPrioSchemas);
                return schemaHandle.getResolvedSchema().then((schema) => {
                    if (schema.schema && typeof schema.schema === 'object') {
                        schema.schema.url = schemaHandle.url;
                    }
                    if (schema.schema &&
                        schema.schema.schemaSequence &&
                        schema.schema.schemaSequence[doc.currentDocIndex]) {
                        return new jsonSchemaService_1.ResolvedSchema(schema.schema.schemaSequence[doc.currentDocIndex]);
                    }
                    return schema;
                });
            }
            return Promise.resolve(null);
        };
        if (this.customSchemaProvider) {
            return this.customSchemaProvider(resource)
                .then((schemaUri) => {
                if (Array.isArray(schemaUri)) {
                    if (schemaUri.length === 0) {
                        return resolveSchema();
                    }
                    return Promise.all(schemaUri.map((schemaUri) => {
                        return this.resolveCustomSchema(schemaUri, doc);
                    })).then((schemas) => {
                        return {
                            errors: [],
                            schema: {
                                anyOf: schemas.map((schemaObj) => {
                                    return schemaObj.schema;
                                }),
                            },
                        };
                    }, () => {
                        return resolveSchema();
                    });
                }
                if (!schemaUri) {
                    return resolveSchema();
                }
                return this.resolveCustomSchema(schemaUri, doc);
            })
                .then((schema) => {
                return schema;
            }, () => {
                return resolveSchema();
            });
        }
        else {
            return resolveSchema();
        }
    }
    // Set the priority of a schema in the schema service
    addSchemaPriority(uri, priority) {
        let currSchemaArray = this.schemaPriorityMapping.get(uri);
        if (currSchemaArray) {
            currSchemaArray = currSchemaArray.add(priority);
            this.schemaPriorityMapping.set(uri, currSchemaArray);
        }
        else {
            this.schemaPriorityMapping.set(uri, new Set().add(priority));
        }
    }
    /**
     * Search through all the schemas and find the ones with the highest priority
     */
    highestPrioritySchemas(schemas) {
        let highestPrio = 0;
        const priorityMapping = new Map();
        schemas.forEach((schema) => {
            // If the schema does not have a priority then give it a default one of [0]
            const priority = this.schemaPriorityMapping.get(schema) || [0];
            priority.forEach((prio) => {
                if (prio > highestPrio) {
                    highestPrio = prio;
                }
                // Build up a mapping of priority to schemas so that we can easily get the highest priority schemas easier
                let currPriorityArray = priorityMapping.get(prio);
                if (currPriorityArray) {
                    currPriorityArray = currPriorityArray.concat(schema);
                    priorityMapping.set(prio, currPriorityArray);
                }
                else {
                    priorityMapping.set(prio, [schema]);
                }
            });
        });
        return priorityMapping.get(highestPrio) || [];
    }
    resolveCustomSchema(schemaUri, doc) {
        return __awaiter(this, void 0, void 0, function* () {
            const unresolvedSchema = yield this.loadSchema(schemaUri);
            const schema = yield this.resolveSchemaContent(unresolvedSchema, schemaUri, []);
            if (schema.schema) {
                schema.schema.url = schemaUri;
            }
            if (schema.schema && schema.schema.schemaSequence && schema.schema.schemaSequence[doc.currentDocIndex]) {
                return new jsonSchemaService_1.ResolvedSchema(schema.schema.schemaSequence[doc.currentDocIndex]);
            }
            return schema;
        });
    }
    /**
     * Save a schema with schema ID and schema content.
     * Overrides previous schemas set for that schema ID.
     */
    saveSchema(schemaId, schemaContent) {
        return __awaiter(this, void 0, void 0, function* () {
            const id = this.normalizeId(schemaId);
            this.getOrAddSchemaHandle(id, schemaContent);
            this.schemaPriorityMapping.set(id, new Set().add(yamlLanguageService_1.SchemaPriority.Settings));
            return Promise.resolve(undefined);
        });
    }
    /**
     * Delete schemas on specific path
     */
    deleteSchemas(deletions) {
        return __awaiter(this, void 0, void 0, function* () {
            deletions.schemas.forEach((s) => {
                this.deleteSchema(s);
            });
            return Promise.resolve(undefined);
        });
    }
    /**
     * Delete a schema with schema ID.
     */
    deleteSchema(schemaId) {
        return __awaiter(this, void 0, void 0, function* () {
            const id = this.normalizeId(schemaId);
            if (this.schemasById[id]) {
                delete this.schemasById[id];
            }
            this.schemaPriorityMapping.delete(id);
            return Promise.resolve(undefined);
        });
    }
    /**
     * Add content to a specified schema at a specified path
     */
    addContent(additions) {
        return __awaiter(this, void 0, void 0, function* () {
            const schema = yield this.getResolvedSchema(additions.schema);
            if (schema) {
                const resolvedSchemaLocation = this.resolveJSONSchemaToSection(schema.schema, additions.path);
                if (typeof resolvedSchemaLocation === 'object') {
                    resolvedSchemaLocation[additions.key] = additions.content;
                }
                yield this.saveSchema(additions.schema, schema.schema);
            }
        });
    }
    /**
     * Delete content in a specified schema at a specified path
     */
    deleteContent(deletions) {
        return __awaiter(this, void 0, void 0, function* () {
            const schema = yield this.getResolvedSchema(deletions.schema);
            if (schema) {
                const resolvedSchemaLocation = this.resolveJSONSchemaToSection(schema.schema, deletions.path);
                if (typeof resolvedSchemaLocation === 'object') {
                    delete resolvedSchemaLocation[deletions.key];
                }
                yield this.saveSchema(deletions.schema, schema.schema);
            }
        });
    }
    /**
     * Take a JSON Schema and the path that you would like to get to
     * @returns the JSON Schema resolved at that specific path
     */
    resolveJSONSchemaToSection(schema, paths) {
        const splitPathway = paths.split('/');
        let resolvedSchemaLocation = schema;
        for (const path of splitPathway) {
            if (path === '') {
                continue;
            }
            this.resolveNext(resolvedSchemaLocation, path);
            resolvedSchemaLocation = resolvedSchemaLocation[path];
        }
        return resolvedSchemaLocation;
    }
    /**
     * Resolve the next Object if they have compatible types
     * @param object a location in the JSON Schema
     * @param token the next token that you want to search for
     */
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    resolveNext(object, token) {
        // eslint-disable-next-line @typescript-eslint/no-explicit-any
        if (Array.isArray(object) && isNaN(token)) {
            throw new Error('Expected a number after the array object');
        }
        else if (typeof object === 'object' && typeof token !== 'string') {
            throw new Error('Expected a string after the object');
        }
    }
    /**
     * Everything below here is needed because we're importing from vscode-json-languageservice umd and we need
     * to provide a wrapper around the javascript methods we are calling since they have no type
     */
    normalizeId(id) {
        // The parent's `super.normalizeId(id)` isn't visible, so duplicated the code here
        try {
            return vscode_uri_1.URI.parse(id).toString();
        }
        catch (e) {
            return id;
        }
    }
    /*
     * Everything below here is needed because we're importing from vscode-json-languageservice umd and we need
     * to provide a wrapper around the javascript methods we are calling since they have no type
     */
    getOrAddSchemaHandle(id, unresolvedSchemaContent) {
        return super.getOrAddSchemaHandle(id, unresolvedSchemaContent);
    }
    loadSchema(schemaUri) {
        const requestService = this.requestService;
        return super.loadSchema(schemaUri).then((unresolvedJsonSchema) => {
            // If json-language-server failed to parse the schema, attempt to parse it as YAML instead.
            if (unresolvedJsonSchema.errors && unresolvedJsonSchema.schema === undefined) {
                return requestService(schemaUri).then((content) => {
                    if (!content) {
                        const errorMessage = localize('json.schema.nocontent', "Unable to load schema from '{0}': No content. {1}", toDisplayString(schemaUri), unresolvedJsonSchema.errors);
                        return new jsonSchemaService_1.UnresolvedSchema({}, [errorMessage]);
                    }
                    try {
                        const schemaContent = (0, yaml_1.parse)(content);
                        return new jsonSchemaService_1.UnresolvedSchema(schemaContent, []);
                    }
                    catch (yamlError) {
                        const errorMessage = localize('json.schema.invalidFormat', "Unable to parse content from '{0}': {1}.", toDisplayString(schemaUri), yamlError);
                        return new jsonSchemaService_1.UnresolvedSchema({}, [errorMessage]);
                    }
                }, 
                // eslint-disable-next-line @typescript-eslint/no-explicit-any
                (error) => {
                    let errorMessage = error.toString();
                    const errorSplit = error.toString().split('Error: ');
                    if (errorSplit.length > 1) {
                        // more concise error message, URL and context are attached by caller anyways
                        errorMessage = errorSplit[1];
                    }
                    return new jsonSchemaService_1.UnresolvedSchema({}, [errorMessage]);
                });
            }
            unresolvedJsonSchema.uri = schemaUri;
            if (this.schemaUriToNameAndDescription.has(schemaUri)) {
                const { name, description, versions } = this.schemaUriToNameAndDescription.get(schemaUri);
                unresolvedJsonSchema.schema.title = name !== null && name !== void 0 ? name : unresolvedJsonSchema.schema.title;
                unresolvedJsonSchema.schema.description = description !== null && description !== void 0 ? description : unresolvedJsonSchema.schema.description;
                unresolvedJsonSchema.schema.versions = versions !== null && versions !== void 0 ? versions : unresolvedJsonSchema.schema.versions;
            }
            return unresolvedJsonSchema;
        });
    }
    registerExternalSchema(uri, filePatterns, unresolvedSchema, name, description, versions) {
        if (name || description) {
            this.schemaUriToNameAndDescription.set(uri, { name, description, versions });
        }
        return super.registerExternalSchema(uri, filePatterns, unresolvedSchema);
    }
    clearExternalSchemas() {
        super.clearExternalSchemas();
    }
    setSchemaContributions(schemaContributions) {
        super.setSchemaContributions(schemaContributions);
    }
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    getRegisteredSchemaIds(filter) {
        return super.getRegisteredSchemaIds(filter);
    }
    getResolvedSchema(schemaId) {
        return super.getResolvedSchema(schemaId);
    }
    onResourceChange(uri) {
        return super.onResourceChange(uri);
    }
}
exports.YAMLSchemaService = YAMLSchemaService;
function toDisplayString(url) {
    try {
        const uri = vscode_uri_1.URI.parse(url);
        if (uri.scheme === 'file') {
            return uri.fsPath;
        }
    }
    catch (e) {
        // ignore
    }
    return url;
}
//# sourceMappingURL=yamlSchemaService.js.map