"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const generate_1 = __importDefault(require("./lib/documentation/generate"));
const tsserverlibrary_1 = require("typescript/lib/tsserverlibrary");
const smart_truncate_1 = __importDefault(require("smart-truncate"));
const markdown_table_1 = __importDefault(require("markdown-table"));
const keywords_1 = require("./constants/keywords");
const get_word_at_offset_1 = __importDefault(require("./lib/get-word-at-offset"));
const mysql_parse_1 = require("mysql-parse");
const autocorrect_1 = __importDefault(require("./lib/autocorrect"));
const analyzer_1 = __importDefault(require("./analyzer"));
const schema_1 = __importDefault(require("./schema"));
const MAX_LENGTH = 30;
function truncate(str) {
    return smart_truncate_1.default(str, MAX_LENGTH);
}
function padding(amount) {
    return '&nbsp;'.repeat(amount);
}
function createRow(column) {
    const pad = padding(5);
    return [
        truncate(column.name),
        pad,
        truncate(column.sqlType),
        pad,
        truncate(column.tsType),
        pad,
        String(column.optional)
    ];
}
class MySqlLanguageService {
    constructor({ host, logger, config }) {
        this.analyzer = new analyzer_1.default(logger);
        this.config = config;
        this.logger = logger;
        this.host = host;
        const { databaseUri } = config;
        if (databaseUri) {
            try {
                const { database: databaseName } = mysql_parse_1.parseUri(databaseUri);
                this.databaseName = databaseName;
                this.schema = new schema_1.default({ databaseName, databaseUri, logger });
                this.schema.on('schemaLoaded', this.onSchemaLoaded.bind(this));
            }
            catch (e) {
                this.logger.log('Failed to parse provided database URI: ' + e);
            }
        }
    }
    // Send `schemaLoadingFinish` to client for testing purposes.
    onSchemaLoaded() {
        const message = {
            type: 'event',
            event: 'schemaLoadingFinish'
        };
        this.sendResponse(message);
    }
    // Send a response to the client.
    sendResponse(message) {
        const json = JSON.stringify(message);
        const len = Buffer.byteLength(json, 'utf8');
        const formattedMessage = `Content-Length: ${1 + len}\r\n\r\n${json}${this.host.newLine}`;
        this.host.write(formattedMessage);
    }
    createDiagnostic(context, input) {
        return {
            file: context.node.getSourceFile(),
            source: this.config.pluginName,
            messageText: input.message,
            category: input.category,
            length: input.length,
            start: input.start,
            code: input.code
        };
    }
    createQuickInfo(input) {
        return {
            kind: tsserverlibrary_1.ScriptElementKind.string,
            kindModifiers: '',
            textSpan: {
                start: input.startOffset,
                length: input.wordAtHover.length
            },
            displayParts: input.header,
            documentation: input.docs,
            tags: []
        };
    }
    hasFileIgnoreComment(context) {
        const contents = this.host.readFile(context.fileName);
        if (!contents) {
            return false;
        }
        const firstLine = contents.split('\n')[0];
        if (firstLine.includes('@ts-mysql-plugin ignore')) {
            return true;
        }
        return false;
    }
    getQuickInfoAtPosition(context, position) {
        if (this.hasFileIgnoreComment(context)) {
            return;
        }
        const offset = context.toOffset(position);
        const wordWithOffset = get_word_at_offset_1.default(offset, context.text);
        if (!wordWithOffset) {
            return;
        }
        const { word, startOffset } = wordWithOffset;
        const wordAtHover = word.toLowerCase();
        const docs = [];
        const header = [];
        if (keywords_1.keywords.includes(wordAtHover)) {
            const documentation = generate_1.default(wordAtHover);
            docs.push({ kind: '', text: documentation });
        }
        const result = this.analyzer.getParseResult(context.text);
        if (!result || !result.statements || !this.schema) {
            return this.createQuickInfo({
                startOffset,
                wordAtHover,
                header,
                docs
            });
        }
        const pad = padding(5);
        const tableHeader = ['Name', pad, 'SQL Type', pad, 'TS Type', pad, 'Optional'];
        const schemaTables = this.schema.getTables();
        const foundColumns = new Map();
        const foundTables = new Map();
        result.statements.forEach(statement => {
            const queryTables = statement.tables;
            const queryTable = queryTables.find(t => t.name === word);
            if (queryTable) {
                const schemaTable = schemaTables.find(t => t.name === queryTable.name);
                // make sure we don't add twice
                if (schemaTable && !foundTables.get(schemaTable.name)) {
                    foundTables.set(schemaTable.name, true);
                    const rows = schemaTable.columns.map(column => createRow(column));
                    docs.push({ kind: '', text: markdown_table_1.default([tableHeader, ...rows]) });
                }
            }
            let schemaColumn = null;
            for (const queryTable of queryTables) {
                const schemaTable = schemaTables.find(t => t.name === queryTable.name);
                if (!schemaTable) {
                    continue;
                }
                const column = schemaTable.columns.find(c => c.name === word);
                if (!column) {
                    continue;
                }
                // make sure we don't add twice
                const alreadyFound = foundColumns.get(`${schemaTable.name}:${column.name}`);
                if (!alreadyFound) {
                    foundColumns.set(`${schemaTable.name}:${column.name}`, true);
                    schemaColumn = column;
                }
                break;
            }
            if (schemaColumn) {
                const row = createRow(schemaColumn);
                docs.push({ kind: '', text: markdown_table_1.default([tableHeader, row]) });
            }
        });
        return this.createQuickInfo({
            startOffset,
            wordAtHover,
            header,
            docs
        });
    }
    getCompletionsAtPosition(context) {
        if (this.hasFileIgnoreComment(context)) {
            return {
                entries: [],
                isNewIdentifierLocation: false,
                isGlobalCompletion: false,
                isMemberCompletion: false
            };
        }
        const keywordEntries = keywords_1.keywords.map((keyword) => {
            return {
                name: keyword.toUpperCase(),
                kind: tsserverlibrary_1.ScriptElementKind.keyword,
                kindModifiers: '',
                sortText: keyword
            };
        });
        if (!this.schema) {
            return {
                entries: keywordEntries,
                isNewIdentifierLocation: false,
                isGlobalCompletion: false,
                isMemberCompletion: false
            };
        }
        const schemaTables = this.schema.getTables();
        const tableEntries = schemaTables.map((table) => {
            return {
                name: table.name,
                kind: tsserverlibrary_1.ScriptElementKind.classElement,
                kindModifiers: '',
                sortText: table.name
            };
        });
        const result = this.analyzer.getParseResult(context.text);
        let columns = [];
        if (result) {
            result.statements.forEach(statement => {
                const queryTables = statement.tables;
                queryTables.forEach(queryTable => {
                    const { name } = queryTable;
                    const schemaTable = schemaTables.find(t => t.name === name);
                    if (schemaTable) {
                        columns = columns.concat(schemaTable.columns);
                    }
                });
            });
        }
        const columnEntries = columns.map((column) => {
            return {
                name: column.name,
                kind: tsserverlibrary_1.ScriptElementKind.memberVariableElement,
                kindModifiers: '',
                sortText: column.name
            };
        });
        return {
            entries: tableEntries.concat(columnEntries).concat(keywordEntries),
            isNewIdentifierLocation: false,
            isGlobalCompletion: false,
            isMemberCompletion: false
        };
    }
    getSemanticDiagnostics(context) {
        var _a;
        this.logger.log('getSemanticDiagnostics');
        const diagnostics = [];
        if (this.hasFileIgnoreComment(context)) {
            return diagnostics;
        }
        let tables = [];
        if (this.schema) {
            tables = this.schema.getTables();
        }
        try {
            this.analyzer.analyze(context, tables);
        }
        catch (error) {
            const { name, data } = error;
            switch (name) {
                case 'EmptyQueryError':
                    diagnostics.push(this.createDiagnostic(context, {
                        message: 'Empty MySQL query.',
                        category: tsserverlibrary_1.DiagnosticCategory.Error,
                        length: 2,
                        start: -1,
                        code: error.code
                    }));
                    break;
                case 'SyntaxErrorGeneric':
                    diagnostics.push(this.createDiagnostic(context, {
                        message: 'MySQL Syntax Error.',
                        category: tsserverlibrary_1.DiagnosticCategory.Error,
                        length: data.end - data.start,
                        start: data.start,
                        code: error.code
                    }));
                    break;
                case 'SyntaxErrorKeyword':
                    diagnostics.push(this.createDiagnostic(context, {
                        message: `MySQL Syntax Error. The problem is near the word '${data.keyword}', which is a reserved keyword. Are you missing a semicolon? Did you forget to backtick a column name?`,
                        category: tsserverlibrary_1.DiagnosticCategory.Error,
                        length: data.end - data.start,
                        start: data.start - 1,
                        code: error.code
                    }));
                    break;
                case 'SyntaxErrorNotKeyword': {
                    const word = data.unidentifiedWord;
                    const correction = this.getCorrection(word.toLowerCase(), keywords_1.keywords);
                    const messageParts = [`MySQL Syntax Error. Unidentified word '${word}'.`];
                    if (correction) {
                        messageParts.push(` Did you mean '${correction.toUpperCase()}'?`);
                    }
                    diagnostics.push(this.createDiagnostic(context, {
                        message: messageParts.join(''),
                        category: tsserverlibrary_1.DiagnosticCategory.Error,
                        length: data.end - data.start,
                        start: data.start - 1,
                        code: error.code
                    }));
                    break;
                }
                case 'SemanticErrorBadTable': {
                    const tableNames = tables.map(t => t.name);
                    const correction = this.getCorrection(data.table.toLowerCase(), tableNames);
                    const messageParts = [`Table '${data.table}' does not exist in database '${this.databaseName}'.`];
                    if (correction) {
                        messageParts.push(` Did you mean '${correction}'?`);
                    }
                    diagnostics.push(this.createDiagnostic(context, {
                        message: messageParts.join(''),
                        category: tsserverlibrary_1.DiagnosticCategory.Warning,
                        length: data.table.length,
                        start: data.start - 1,
                        code: error.code
                    }));
                    break;
                }
                case 'SemanticErrorBadColumn': {
                    const table = tables.find(t => t.name === data.table);
                    const columnNames = ((_a = table) === null || _a === void 0 ? void 0 : _a.columns.map(c => c.name)) || [];
                    const correction = this.getCorrection(data.column.toLowerCase(), columnNames);
                    const messageParts = [`Column '${data.column}' does not exist in table '${data.table}'.`];
                    if (correction) {
                        messageParts.push(` Did you mean '${correction}'?`);
                    }
                    diagnostics.push(this.createDiagnostic(context, {
                        message: messageParts.join(''),
                        category: tsserverlibrary_1.DiagnosticCategory.Warning,
                        length: data.column.length,
                        start: data.start - 1,
                        code: error.code
                    }));
                    break;
                }
                case 'SemanticErrorBadColumnValue': {
                    diagnostics.push(this.createDiagnostic(context, {
                        message: `Type ${data.receivedType} is not assignable to type ${data.expectedType}`,
                        category: tsserverlibrary_1.DiagnosticCategory.Warning,
                        length: data.length,
                        start: data.start - 1,
                        code: error.code
                    }));
                    break;
                }
            }
        }
        return diagnostics;
    }
    getCorrection(near, keywords) {
        const correction = autocorrect_1.default(near, keywords);
        if (!correction) {
            return '';
        }
        if (correction === near) {
            return '';
        }
        return correction;
    }
}
exports.default = MySqlLanguageService;
//# sourceMappingURL=language-service.js.map