"use strict";
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseToSyntaxTrees = exports.parseNonGremlinCodeToSyntaxTree = void 0;
const types_1 = require("../types");
const utils_1 = require("../utils");
const extractGremlinQueries_1 = require("./extractGremlinQueries");
const tokenizeOnTopLevelPunctuation = (query) => {
    let word = '';
    let parenthesesCount = 0;
    let squareBracketCount = 0;
    let curlyBracketCount = 0;
    let isInsideSingleQuoteString = false;
    query.split('').forEach((char) => {
        if (char === '(' && !isInsideSingleQuoteString) {
            parenthesesCount++;
            word += '(';
            return;
        }
        if (char === '[' && !isInsideSingleQuoteString) {
            squareBracketCount++;
            word += '[';
            return;
        }
        if (char === '{' && !isInsideSingleQuoteString) {
            curlyBracketCount++;
            word += '{';
            return;
        }
        if (char === ')' && !isInsideSingleQuoteString) {
            parenthesesCount--;
            word += ')';
            return;
        }
        if (char === ']' && !isInsideSingleQuoteString) {
            squareBracketCount--;
            word += ']';
            return;
        }
        if (char === '}' && !isInsideSingleQuoteString) {
            curlyBracketCount--;
            word += '}';
            return;
        }
        if (char === "'") {
            isInsideSingleQuoteString = !isInsideSingleQuoteString;
            word += "'";
            return;
        }
        if (char === '.') {
            word +=
                isInsideSingleQuoteString || parenthesesCount || squareBracketCount || curlyBracketCount
                    ? '.'
                    : String.fromCharCode(28);
            return;
        }
        word += char;
    });
    return word
        .split(String.fromCharCode(28))
        .filter((token) => token !== '')
        .map((token) => token.trim());
};
const tokenizeOnTopLevelComma = (query) => {
    let word = '';
    let parenthesesCount = 0;
    let squareBracketsCount = 0;
    let curlyBracketsCount = 0;
    let isInsideSingleQuoteString = false;
    query.split('').forEach((char) => {
        if (char === '(' && !isInsideSingleQuoteString) {
            parenthesesCount++;
            word += '(';
            return;
        }
        if (char === '[' && !isInsideSingleQuoteString) {
            squareBracketsCount++;
            word += '[';
            return;
        }
        if (char === '{' && !isInsideSingleQuoteString) {
            curlyBracketsCount++;
            word += '{';
            return;
        }
        if (char === ')' && !isInsideSingleQuoteString) {
            parenthesesCount--;
            word += ')';
            return;
        }
        if (char === ']' && !isInsideSingleQuoteString) {
            squareBracketsCount--;
            word += ']';
            return;
        }
        if (char === '}' && !isInsideSingleQuoteString) {
            curlyBracketsCount--;
            word += '}';
            return;
        }
        if (char === "'") {
            isInsideSingleQuoteString = !isInsideSingleQuoteString;
            word += "'";
            return;
        }
        if (char === ',') {
            word +=
                isInsideSingleQuoteString || parenthesesCount || squareBracketsCount || curlyBracketsCount
                    ? ','
                    : String.fromCharCode(28);
            return;
        }
        word += char;
    });
    return word
        .split(String.fromCharCode(28))
        .filter((token) => token !== '')
        .map((token) => token.trim());
};
const tokenizeOnTopLevelParentheses = (query) => {
    let word = '';
    let parenthesesCount = 0;
    let squareBracketsCount = 0;
    let curlyBracketsCount = 0;
    let isInsideSingleQuoteString = false;
    query.split('').forEach((char) => {
        if (char === '(' && !isInsideSingleQuoteString) {
            if (parenthesesCount === 0) {
                word += String.fromCharCode(28);
            }
            parenthesesCount++;
            word += '(';
            return;
        }
        if (char === '[' && !isInsideSingleQuoteString) {
            squareBracketsCount++;
            word += '[';
            return;
        }
        if (char === '{' && !isInsideSingleQuoteString) {
            curlyBracketsCount++;
            word += '{';
            return;
        }
        if (char === ')' && !isInsideSingleQuoteString) {
            parenthesesCount--;
            word += ')';
            return;
        }
        if (char === ']' && !isInsideSingleQuoteString) {
            squareBracketsCount--;
            word += ']';
            return;
        }
        if (char === '}' && !isInsideSingleQuoteString) {
            curlyBracketsCount--;
            word += '}';
            return;
        }
        if (char === "'") {
            isInsideSingleQuoteString = !isInsideSingleQuoteString;
            word += "'";
            return;
        }
        word += char;
    });
    return word
        .split(String.fromCharCode(28))
        .filter((token) => token !== '')
        .map((token) => token.trim());
};
const tokenizeOnTopLevelCurlyBrackets = (query) => {
    let word = '';
    let parenthesesCount = 0;
    let squareBracketsCount = 0;
    let curlyBracketsCount = 0;
    let isInsideSingleQuoteString = false;
    query.split('').forEach((char) => {
        if (char === '(' && !isInsideSingleQuoteString) {
            parenthesesCount++;
            word += '(';
            return;
        }
        if (char === '[' && !isInsideSingleQuoteString) {
            squareBracketsCount++;
            word += '[';
            return;
        }
        if (char === '{' && !isInsideSingleQuoteString) {
            if (curlyBracketsCount === 0) {
                word += String.fromCharCode(28);
            }
            curlyBracketsCount++;
            word += '{';
            return;
        }
        if (char === ')' && !isInsideSingleQuoteString) {
            parenthesesCount--;
            word += ')';
            return;
        }
        if (char === ']' && !isInsideSingleQuoteString) {
            squareBracketsCount--;
            word += ']';
            return;
        }
        if (char === '}' && !isInsideSingleQuoteString) {
            curlyBracketsCount--;
            word += '}';
            return;
        }
        if (char === "'") {
            isInsideSingleQuoteString = !isInsideSingleQuoteString;
            word += "'";
            return;
        }
        word += char;
    });
    return word
        .split(String.fromCharCode(28))
        .filter((token) => token !== '')
        .map((token) => token.trim());
};
const isWrappedInParentheses = (token) => {
    if (token.length < 2)
        return false;
    if (token.charAt(0) !== '(')
        return false;
    if (token.slice(-1) !== ')')
        return false;
    return true;
};
const isWrappedInCurlyBrackets = (token) => {
    if (token.length < 2)
        return false;
    if (token.charAt(0) !== '{')
        return false;
    if (token.slice(-1) !== '}')
        return false;
    return true;
};
const isString = (token) => {
    if (token.length < 2)
        return false;
    if (token.charAt(0) !== token.substr(-1))
        return false;
    if (['"', "'"].includes(token.charAt(0)))
        return true;
    return false;
};
const isMethodInvocation = (token) => {
    return (0, utils_1.pipe)(tokenizeOnTopLevelParentheses, utils_1.last, isWrappedInParentheses)(token);
};
const isClosureInvocation = (token) => {
    return (0, utils_1.pipe)(tokenizeOnTopLevelCurlyBrackets, utils_1.last, isWrappedInCurlyBrackets)(token);
};
const trimParentheses = (expression) => expression.slice(1, -1);
const trimCurlyBrackets = (expression) => expression.slice(1, -1);
const getMethodTokenAndArgumentTokensFromMethodInvocation = (token) => {
    // The word before the first parenthesis is the method name
    // The token may be a double application of a curried function, so we cannot
    // assume that the first opening parenthesis is closed by the last closing
    // parenthesis
    const tokens = tokenizeOnTopLevelParentheses(token);
    return {
        methodToken: tokens.slice(0, -1).join(''),
        argumentTokens: (0, utils_1.pipe)(trimParentheses, tokenizeOnTopLevelComma)(tokens.slice(-1)[0]),
    };
};
const getIndentation = (lineOfCode) => lineOfCode.split('').findIndex((0, utils_1.neq)(' '));
const getMethodTokenAndClosureCodeBlockFromClosureInvocation = (token, fullQuery) => {
    // The word before the first curly bracket is the method name
    // The token may be a double application of a curried function, so we cannot
    // assume that the first opening curly bracket is closed by the last closing
    // curly bracket
    const tokens = tokenizeOnTopLevelCurlyBrackets(token);
    const methodToken = tokens.slice(0, -1).join('');
    const closureCodeBlockToken = trimCurlyBrackets(tokens.slice(-1)[0]);
    const initialClosureCodeBlockIndentation = fullQuery
        .substr(0, fullQuery.indexOf(closureCodeBlockToken))
        .split('\n')
        .slice(-1)[0].length;
    return {
        methodToken,
        closureCodeBlock: trimCurlyBrackets(tokens.slice(-1)[0])
            .split('\n')
            .map((lineOfCode, i) => ({
            lineOfCode: lineOfCode.trimStart(),
            relativeIndentation: i === 0 ? getIndentation(lineOfCode) : getIndentation(lineOfCode) - initialClosureCodeBlockIndentation,
        })),
    };
};
const parseCodeBlockToSyntaxTree = (fullCode, shouldCalculateInitialHorizontalPosition) => (codeBlock) => {
    const tokens = tokenizeOnTopLevelPunctuation(codeBlock);
    if (tokens.length === 1) {
        const token = tokens[0];
        if (isMethodInvocation(token)) {
            const { methodToken, argumentTokens } = getMethodTokenAndArgumentTokensFromMethodInvocation(token);
            return {
                type: types_1.TokenType.Method,
                method: parseCodeBlockToSyntaxTree(fullCode)(methodToken),
                arguments: argumentTokens.map(parseCodeBlockToSyntaxTree(fullCode)),
            };
        }
        if (isClosureInvocation(token)) {
            const { methodToken, closureCodeBlock } = getMethodTokenAndClosureCodeBlockFromClosureInvocation(token, fullCode);
            return {
                type: types_1.TokenType.Closure,
                method: parseCodeBlockToSyntaxTree(fullCode)(methodToken),
                closureCodeBlock,
            };
        }
        if (isString(token)) {
            return {
                type: types_1.TokenType.String,
                string: token,
            };
        }
        return {
            type: types_1.TokenType.Word,
            word: token,
        };
    }
    return {
        type: types_1.TokenType.Traversal,
        steps: tokens.map(parseCodeBlockToSyntaxTree(fullCode)),
        initialHorizontalPosition: shouldCalculateInitialHorizontalPosition
            ? fullCode.substr(0, fullCode.indexOf(codeBlock)).split('\n').slice(-1)[0].length
            : 0,
    };
};
const parseNonGremlinCodeToSyntaxTree = (code) => ({
    type: types_1.TokenType.NonGremlinCode,
    code,
});
exports.parseNonGremlinCodeToSyntaxTree = parseNonGremlinCodeToSyntaxTree;
const parseToSyntaxTrees = (code) => {
    const queries = (0, extractGremlinQueries_1.extractGremlinQueries)(code);
    const { syntaxTrees, remainingCode } = queries.reduce((state, query) => {
        const indexOfQuery = state.remainingCode.indexOf(query);
        const nonGremlinCode = state.remainingCode.substr(0, indexOfQuery);
        return {
            syntaxTrees: [
                ...state.syntaxTrees,
                (0, exports.parseNonGremlinCodeToSyntaxTree)(nonGremlinCode),
                parseCodeBlockToSyntaxTree(code, true)(query),
            ],
            remainingCode: state.remainingCode.substr(indexOfQuery + query.length),
        };
    }, { syntaxTrees: [], remainingCode: code });
    if (!remainingCode)
        return syntaxTrees;
    return [...syntaxTrees, (0, exports.parseNonGremlinCodeToSyntaxTree)(remainingCode)];
};
exports.parseToSyntaxTrees = parseToSyntaxTrees;
