/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Red Hat, Inc. All rights reserved.
 *  Copyright (c) Adam Voss. 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';
Object.defineProperty(exports, "__esModule", { value: true });
const vscode_languageserver_1 = require("vscode-languageserver");
const request_light_1 = require("request-light");
const URL = require("url");
const arrUtils_1 = require("./languageservice/utils/arrUtils");
const yamlLanguageService_1 = require("./languageservice/yamlLanguageService");
const nls = require("vscode-nls");
const jsonSchemaService_1 = require("./languageservice/services/jsonSchemaService");
const requestTypes_1 = require("./requestTypes");
const schemaRequestHandler_1 = require("./languageservice/services/schemaRequestHandler");
const paths_1 = require("./languageservice/utils/paths");
const vscode_uri_1 = require("vscode-uri");
// tslint:disable-next-line: no-any
nls.config(process.env['VSCODE_NLS_CONFIG']);
/****************
 * Constants
 ****************/
const KUBERNETES_SCHEMA_URL = 'https://raw.githubusercontent.com/garethr/kubernetes-json-schema/master/v1.14.0-standalone-strict/all.json';
const JSON_SCHEMASTORE_URL = 'http://schemastore.org/api/json/catalog.json';
/**************************
 * Generic helper functions
 **************************/
const workspaceContext = {
    resolveRelativePath: (relativePath, resource) => URL.resolve(resource, relativePath)
};
/****************
 * Variables
 ****************/
// Language server configuration
let yamlConfigurationSettings = void 0;
let schemaAssociations = void 0;
let formatterRegistration = null;
let specificValidatorPaths = [];
let schemaConfigurationSettings = [];
let yamlShouldValidate = true;
let yamlFormatterSettings = {
    singleQuote: false,
    bracketSpacing: true,
    proseWrap: 'preserve',
    printWidth: 80,
    enable: true
};
let yamlShouldHover = true;
let yamlShouldCompletion = true;
let schemaStoreSettings = [];
let customTags = [];
let schemaStoreEnabled = true;
// File validation helpers
const pendingValidationRequests = {};
const validationDelayMs = 200;
// Create a simple text document manager. The text document manager
// supports full document sync only
const documents = new vscode_languageserver_1.TextDocuments();
// Language client configuration
let capabilities;
let workspaceRoot = null;
let workspaceFolders = [];
let clientDynamicRegisterSupport = false;
let hierarchicalDocumentSymbolSupport = false;
/****************************
 * Reusable helper functions
 ****************************/
const checkSchemaURI = (uri) => {
    if (uri.trim().toLowerCase() === 'kubernetes') {
        return KUBERNETES_SCHEMA_URL;
    }
    else if (paths_1.isRelativePath(uri)) {
        return paths_1.relativeToAbsolutePath(workspaceFolders, workspaceRoot, uri);
    }
    else {
        return uri;
    }
};
/**
 * This function helps set the schema store if it hasn't already been set
 * AND the schema store setting is enabled. If the schema store setting
 * is not enabled we need to clear the schemas.
 */
function setSchemaStoreSettingsIfNotSet() {
    const schemaStoreIsSet = (schemaStoreSettings.length !== 0);
    if (schemaStoreEnabled && !schemaStoreIsSet) {
        getSchemaStoreMatchingSchemas().then(schemaStore => {
            schemaStoreSettings = schemaStore.schemas;
            updateConfiguration();
        });
    }
    else if (!schemaStoreEnabled) {
        schemaStoreSettings = [];
        updateConfiguration();
    }
}
/**
 * When the schema store is enabled, download and store YAML schema associations
 */
function getSchemaStoreMatchingSchemas() {
    return request_light_1.xhr({ url: JSON_SCHEMASTORE_URL }).then(response => {
        const languageSettings = {
            schemas: []
        };
        // Parse the schema store catalog as JSON
        const schemas = JSON.parse(response.responseText);
        for (const schemaIndex in schemas.schemas) {
            const schema = schemas.schemas[schemaIndex];
            if (schema && schema.fileMatch) {
                for (const fileMatch in schema.fileMatch) {
                    const currFileMatch = schema.fileMatch[fileMatch];
                    // If the schema is for files with a YAML extension, save the schema association
                    if (currFileMatch.indexOf('.yml') !== -1 || currFileMatch.indexOf('.yaml') !== -1) {
                        languageSettings.schemas.push({ uri: schema.url, fileMatch: [currFileMatch] });
                    }
                }
            }
        }
        return languageSettings;
    }, (error) => {
        throw error;
    });
}
/**
 * Called when server settings or schema associations are changed
 * Re-creates schema associations and revalidates any open YAML files
 */
function updateConfiguration() {
    let languageSettings = {
        validate: yamlShouldValidate,
        hover: yamlShouldHover,
        completion: yamlShouldCompletion,
        schemas: [],
        customTags: customTags,
        format: yamlFormatterSettings.enable
    };
    if (schemaAssociations) {
        for (const pattern in schemaAssociations) {
            const association = schemaAssociations[pattern];
            if (Array.isArray(association)) {
                association.forEach(uri => {
                    languageSettings = configureSchemas(uri, [pattern], null, languageSettings);
                });
            }
        }
    }
    if (schemaConfigurationSettings) {
        schemaConfigurationSettings.forEach(schema => {
            let uri = schema.uri;
            if (!uri && schema.schema) {
                uri = schema.schema.id;
            }
            if (!uri && schema.fileMatch) {
                uri = 'vscode://schemas/custom/' + encodeURIComponent(schema.fileMatch.join('&'));
            }
            if (uri) {
                if (paths_1.isRelativePath(uri)) {
                    uri = paths_1.relativeToAbsolutePath(workspaceFolders, workspaceRoot, uri);
                }
                languageSettings = configureSchemas(uri, schema.fileMatch, schema.schema, languageSettings);
            }
        });
    }
    if (schemaStoreSettings) {
        languageSettings.schemas = languageSettings.schemas.concat(schemaStoreSettings);
    }
    exports.customLanguageService.configure(languageSettings);
    // Revalidate any open text documents
    documents.all().forEach(triggerValidation);
}
/**
 * Stores schema associations in server settings, handling kubernetes
 * @param uri string path to schema (whether local or online)
 * @param fileMatch file pattern to apply the schema to
 * @param schema schema id
 * @param languageSettings current server settings
 */
function configureSchemas(uri, fileMatch, schema, languageSettings) {
    uri = checkSchemaURI(uri);
    if (schema === null) {
        languageSettings.schemas.push({ uri, fileMatch: fileMatch });
    }
    else {
        languageSettings.schemas.push({ uri, fileMatch: fileMatch, schema: schema });
    }
    if (fileMatch.constructor === Array && uri === KUBERNETES_SCHEMA_URL) {
        fileMatch.forEach(url => {
            specificValidatorPaths.push(url);
        });
    }
    else if (uri === KUBERNETES_SCHEMA_URL) {
        specificValidatorPaths.push(fileMatch);
    }
    return languageSettings;
}
function isKubernetes(textDocument) {
    for (const path in specificValidatorPaths) {
        const globPath = specificValidatorPaths[path];
        const fpa = new jsonSchemaService_1.FilePatternAssociation(globPath);
        if (fpa.matchesPattern(textDocument.uri)) {
            return true;
        }
    }
    return false;
}
function cleanPendingValidation(textDocument) {
    const request = pendingValidationRequests[textDocument.uri];
    if (request) {
        clearTimeout(request);
        delete pendingValidationRequests[textDocument.uri];
    }
}
function triggerValidation(textDocument) {
    cleanPendingValidation(textDocument);
    pendingValidationRequests[textDocument.uri] = setTimeout(() => {
        delete pendingValidationRequests[textDocument.uri];
        validateTextDocument(textDocument);
    }, validationDelayMs);
}
function validateTextDocument(textDocument) {
    if (!textDocument) {
        return;
    }
    if (textDocument.getText().length === 0) {
        connection.sendDiagnostics({ uri: textDocument.uri, diagnostics: [] });
        return;
    }
    exports.customLanguageService.doValidation(textDocument, isKubernetes(textDocument))
        .then(function (diagnosticResults) {
        const diagnostics = [];
        for (const diagnosticItem in diagnosticResults) {
            diagnosticResults[diagnosticItem].severity = 1; //Convert all warnings to errors
            diagnostics.push(diagnosticResults[diagnosticItem]);
        }
        connection.sendDiagnostics({ uri: textDocument.uri, diagnostics: arrUtils_1.removeDuplicatesObj(diagnostics) });
    }, function (error) { });
}
/*************
 * Main setup
 *************/
// Create a connection for the server.
let connection = null;
if (process.argv.indexOf('--stdio') === -1) {
    connection = vscode_languageserver_1.createConnection(vscode_languageserver_1.ProposedFeatures.all);
}
else {
    connection = vscode_languageserver_1.createConnection();
}
console.log = connection.console.log.bind(connection.console);
console.error = connection.console.error.bind(connection.console);
// Make the text document manager listen on the connection
// for open, change and close text document events
documents.listen(connection);
const schemaRequestService = schemaRequestHandler_1.schemaRequestHandler.bind(this, connection);
exports.customLanguageService = yamlLanguageService_1.getLanguageService(schemaRequestService, workspaceContext, []);
/***********************
 * Connection listeners
 **********************/
/**
 * Run when the client connects to the server after it is activated.
 * The server receives the root path(s) of the workspace and the client capabilities.
 */
connection.onInitialize((params) => {
    capabilities = params.capabilities;
    workspaceRoot = vscode_uri_1.URI.parse(params.rootUri);
    workspaceFolders = params.workspaceFolders;
    hierarchicalDocumentSymbolSupport = !!capabilities.textDocument.documentSymbol.hierarchicalDocumentSymbolSupport;
    clientDynamicRegisterSupport = !!capabilities.workspace.symbol.dynamicRegistration;
    return {
        capabilities: {
            textDocumentSync: documents.syncKind,
            completionProvider: { resolveProvider: true },
            hoverProvider: true,
            documentSymbolProvider: true,
            documentFormattingProvider: true
        }
    };
});
/**
 * Received a notification from the client with schema associations from other extensions
 * Update the associations in the server
 */
connection.onNotification(requestTypes_1.SchemaAssociationNotification.type, associations => {
    schemaAssociations = associations;
    specificValidatorPaths = [];
    setSchemaStoreSettingsIfNotSet();
    updateConfiguration();
});
/**
 * Received a notification from the client that it can accept custom schema requests
 * Register the custom schema provider and use it for requests of unknown scheme
 */
connection.onNotification(requestTypes_1.DynamicCustomSchemaRequestRegistration.type, () => {
    const schemaProvider = (resource => connection.sendRequest(requestTypes_1.CustomSchemaRequest.type, resource));
    exports.customLanguageService.registerCustomSchemaProvider(schemaProvider);
});
/**
 * Run when the editor configuration is changed
 * The client syncs the 'yaml', 'http.proxy', 'http.proxyStrictSSL' settings sections
 * Update relevant settings with fallback to defaults if needed
 */
connection.onDidChangeConfiguration(change => {
    const settings = change.settings;
    request_light_1.configure(settings.http && settings.http.proxy, settings.http && settings.http.proxyStrictSSL);
    specificValidatorPaths = [];
    if (settings.yaml) {
        yamlConfigurationSettings = settings.yaml.schemas;
        yamlShouldValidate = settings.yaml.validate;
        yamlShouldHover = settings.yaml.hover;
        yamlShouldCompletion = settings.yaml.completion;
        customTags = settings.yaml.customTags ? settings.yaml.customTags : [];
        if (settings.yaml.schemaStore) {
            schemaStoreEnabled = settings.yaml.schemaStore.enable;
        }
        if (settings.yaml.format) {
            yamlFormatterSettings = {
                proseWrap: settings.yaml.format.proseWrap || 'preserve',
                printWidth: settings.yaml.format.printWidth || 80
            };
            if (settings.yaml.format.singleQuote !== undefined) {
                yamlFormatterSettings.singleQuote = settings.yaml.format.singleQuote;
            }
            if (settings.yaml.format.bracketSpacing !== undefined) {
                yamlFormatterSettings.bracketSpacing = settings.yaml.format.bracketSpacing;
            }
            if (settings.yaml.format.enable !== undefined) {
                yamlFormatterSettings.enable = settings.yaml.format.enable;
            }
        }
    }
    schemaConfigurationSettings = [];
    for (const uri in yamlConfigurationSettings) {
        const globPattern = yamlConfigurationSettings[uri];
        const schemaObj = {
            'fileMatch': Array.isArray(globPattern) ? globPattern : [globPattern],
            'uri': checkSchemaURI(uri)
        };
        schemaConfigurationSettings.push(schemaObj);
    }
    setSchemaStoreSettingsIfNotSet();
    updateConfiguration();
    // dynamically enable & disable the formatter
    if (clientDynamicRegisterSupport) {
        const enableFormatter = settings && settings.yaml && settings.yaml.format && settings.yaml.format.enable;
        if (enableFormatter) {
            if (!formatterRegistration) {
                formatterRegistration = connection.client.register(vscode_languageserver_1.DocumentRangeFormattingRequest.type, { documentSelector: [{ language: 'yaml' }] });
            }
        }
        else if (formatterRegistration) {
            formatterRegistration.then(r => r.dispose());
            formatterRegistration = null;
        }
    }
});
documents.onDidChangeContent(change => {
    triggerValidation(change.document);
});
documents.onDidClose(event => {
    cleanPendingValidation(event.document);
    connection.sendDiagnostics({ uri: event.document.uri, diagnostics: [] });
});
/**
 * Called when a monitored file is changed in an editor
 * Revalidates the entire document
 */
connection.onDidChangeWatchedFiles(change => {
    let hasChanges = false;
    change.changes.forEach(c => {
        if (exports.customLanguageService.resetSchema(c.uri)) {
            hasChanges = true;
        }
    });
    if (hasChanges) {
        documents.all().forEach(validateTextDocument);
    }
});
/**
 * Called when auto-complete is triggered in an editor
 * Returns a list of valid completion items
 */
connection.onCompletion(textDocumentPosition => {
    const textDocument = documents.get(textDocumentPosition.textDocument.uri);
    const result = {
        items: [],
        isIncomplete: false
    };
    if (!textDocument) {
        return Promise.resolve(result);
    }
    return exports.customLanguageService.doComplete(textDocument, textDocumentPosition.position, isKubernetes(textDocument));
});
/**
 * Like onCompletion, but called only for currently selected completion item
 * Provides additional information about the item, not just the keyword
 */
connection.onCompletionResolve(completionItem => exports.customLanguageService.doResolve(completionItem));
/**
 * Called when the user hovers with their mouse over a keyword
 * Returns an informational tooltip
 */
connection.onHover(textDocumentPositionParams => {
    const document = documents.get(textDocumentPositionParams.textDocument.uri);
    if (!document) {
        return Promise.resolve(void 0);
    }
    return exports.customLanguageService.doHover(document, textDocumentPositionParams.position);
});
/**
 * Called when the code outline in an editor needs to be populated
 * Returns a list of symbols that is then shown in the code outline
 */
connection.onDocumentSymbol(documentSymbolParams => {
    const document = documents.get(documentSymbolParams.textDocument.uri);
    if (!document) {
        return;
    }
    if (hierarchicalDocumentSymbolSupport) {
        return exports.customLanguageService.findDocumentSymbols2(document);
    }
    else {
        return exports.customLanguageService.findDocumentSymbols(document);
    }
});
/**
 * Called when the formatter is invoked
 * Returns the formatted document content using prettier
 */
connection.onDocumentFormatting(formatParams => {
    const document = documents.get(formatParams.textDocument.uri);
    if (!document) {
        return;
    }
    const customFormatterSettings = Object.assign({ tabWidth: formatParams.options.tabSize }, yamlFormatterSettings);
    return exports.customLanguageService.doFormat(document, customFormatterSettings);
});
// Start listening for any messages from the client
connection.listen();
//# sourceMappingURL=server.js.map