"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.BaseEditorInput = void 0;
const tslib_1 = require("tslib");
const rebass_1 = require("rebass");
const parser = tslib_1.__importStar(require("../../parser/src"));
const React = tslib_1.__importStar(require("react"));
const ErrorBox_1 = tslib_1.__importDefault(require("../ErrorBox"));
const useSelectionRange_1 = require("../utils/useSelectionRange");
const EditorButton_1 = tslib_1.__importDefault(require("./EditorButton"));
const HelpBlock_1 = tslib_1.__importDefault(require("../HelpBlock"));
const OverlayTrigger_1 = tslib_1.__importDefault(require("../OverlayTrigger"));
const SimpleButton_1 = tslib_1.__importDefault(require("../SimpleButton"));
const Textarea_1 = tslib_1.__importDefault(require("../Textarea"));
const react_1 = require("react");
const ExpressionEvaluationError_1 = require("../../parser/src/ExpressionEvaluationError");
const ButtonInfo_1 = require("../../View/Components/Buttons/ButtonInfo");
const DocumentationLinkConstants_1 = require("../../Utilities/Constants/DocumentationLinkConstants");
const icons_1 = require("../icons");
const AdaptableContext_1 = require("../../View/AdaptableContext");
const join_1 = tslib_1.__importDefault(require("../utils/join"));
const ExpressionFunctionDocumentation_1 = require("./ExpressionFunctionDocumentation");
const Tag_1 = require("../Tag");
const StringExtensions_1 = tslib_1.__importDefault(require("../../Utilities/Extensions/StringExtensions"));
const Radio_1 = tslib_1.__importDefault(require("../Radio"));
const filterableCategories = [
    'dates',
    'logical',
    'maths',
    'strings',
    'comparison',
    'observable',
    'aggregation',
    'cumulative',
];
const getCategoryOrder = (category) => {
    const predefinedOrder = {
        special: 1,
        conditional: 2,
        logical: 3,
        comparison: 4,
        strings: 5,
        maths: 6,
        dates: 7,
    };
    return predefinedOrder[category] || 0;
};
const VarEditorButton = () => {
    const adaptable = (0, AdaptableContext_1.useAdaptable)();
    const customQueryVariables = adaptable.api.optionsApi.getExpressionOptions()?.customQueryVariables;
    if (!customQueryVariables || Object.keys(customQueryVariables).length === 0) {
        return React.createElement(React.Fragment, null);
    }
    return (React.createElement(React.Fragment, null, Object.keys(customQueryVariables).map((varOption) => {
        const varString = `VAR('${varOption}')`;
        return (React.createElement(EditorButton_1.default, { data: varString, key: varOption }, varString));
    })));
};
const FunctionsDropdown = ({ expressionFunctions, baseClassName }) => {
    const [currentFunctionCategory, setCurrentFunctionCategory] = React.useState('all');
    const dropdownRef = React.useRef(null);
    const [overFunction, setOverFunction] = React.useState();
    React.useEffect(() => {
        setOverFunction(null);
    }, [currentFunctionCategory]);
    const groupedFunctions = React.useMemo(() => {
        return Object.keys(expressionFunctions).reduce((acc, key) => {
            const functionDef = expressionFunctions[key];
            // filter
            if (currentFunctionCategory !== 'all' && functionDef.category !== currentFunctionCategory) {
                return acc;
            }
            if (functionDef.category) {
                acc[functionDef.category] = {
                    ...acc[functionDef.category],
                    [key]: functionDef,
                };
            }
            else {
                acc.noCategory[key] = functionDef;
            }
            return acc;
        }, { noCategory: {} });
    }, [currentFunctionCategory]);
    const orderedGroupNames = React.useMemo(() => {
        return Object.keys(groupedFunctions).sort((first, second) => getCategoryOrder(first) - getCategoryOrder(second));
    }, [groupedFunctions]);
    const handleFunctionCategoryChange = React.useCallback((type) => (event) => {
        setCurrentFunctionCategory(type);
    }, [currentFunctionCategory]);
    const categoryOptions = React.useMemo(() => {
        const categoryOptions = Object.keys(expressionFunctions)
            .reduce((acc, functionName) => {
            const functionExpression = expressionFunctions[functionName];
            if (!acc.includes(functionExpression.category)) {
                acc.push(functionExpression.category);
            }
            return acc;
        }, [])
            .filter((category) => filterableCategories.includes(category))
            .sort((first, second) => getCategoryOrder(first) - getCategoryOrder(second))
            .map((category) => ({
            label: StringExtensions_1.default.Humanize(category),
            value: category,
        }));
        return [{ label: 'All', value: 'all' }, ...categoryOptions];
    }, [expressionFunctions]);
    const hidePopup = () => {
        dropdownRef.current.hide();
        setOverFunction(null);
    };
    /**
     * Hide when:
     * - mouse leaves
     * - a function is inserted
     */
    return (React.createElement(OverlayTrigger_1.default, { ref: dropdownRef, showEvent: "mouseenter", hideEvent: "mouseleave", targetOffset: 5, render: () => (React.createElement(rebass_1.Flex, { className: `${baseClassName}__dropdown-functions-list-wrapper`, flexDirection: "column", onMouseLeave: () => hidePopup() },
            React.createElement(rebass_1.Flex, { pl: 2, style: { gap: 10 } }, categoryOptions.map((option, index) => {
                return (React.createElement(Radio_1.default, { key: option.value, onFocus: (event) => {
                        event.preventDefault();
                        event.stopPropagation();
                    }, onClick: handleFunctionCategoryChange(option.value), checked: currentFunctionCategory === option.value }, option.label));
            })),
            React.createElement(rebass_1.Flex, null,
                React.createElement(rebass_1.Flex, { className: `${baseClassName}__dropdown-functions-list`, "data-name": "expression-dropdown-functions-list", flexDirection: "column", p: 2, maxHeight: '50vh' }, orderedGroupNames
                    .filter((groupName) => !!groupedFunctions[groupName])
                    .map((groupName) => {
                    const functionsInGroup = Object.keys(groupedFunctions[groupName]);
                    if (functionsInGroup.length === 0) {
                        return React.createElement(React.Fragment, { key: groupName });
                    }
                    const getEditorButtonData = (functionName) => {
                        // handle special cases
                        if (functionName === 'CASE') {
                            return `CASE <caseValue> WHEN <whenValue> THEN <thenValue> ELSE <defaultValue> END`;
                        }
                        if (functionName === 'TRUE' || functionName === 'FALSE') {
                            return functionName;
                        }
                        if (functionName === 'IF') {
                            return `<condition_expr> ? <consequent_expr> : <alternative_expr>`;
                        }
                        return `${functionName}()`;
                    };
                    return (React.createElement(rebass_1.Box, { mb: 2, key: groupName },
                        React.createElement(Tag_1.Tag, { mb: 1 }, StringExtensions_1.default.Humanize(groupName)),
                        functionsInGroup.map((functionName) => (React.createElement(rebass_1.Box, { key: functionName, onMouseEnter: () => setOverFunction(functionName), onClick: () => hidePopup() }, functionName === 'VAR' ? (React.createElement(VarEditorButton, { key: functionName })) : (React.createElement(EditorButton_1.default, { data: getEditorButtonData(functionName), key: functionName, mr: 1 }, functionName)))))));
                })),
                React.createElement(rebass_1.Box, { className: `${baseClassName}__dropdown-functions-description`, p: 2, width: 600, maxWidth: "30vw" }, overFunction ? (React.createElement(React.Fragment, null,
                    React.createElement(Tag_1.Tag, null, overFunction),
                    React.createElement(ExpressionFunctionDocumentation_1.ExpressionFunctionDocumentation, { expressionFunction: expressionFunctions[overFunction] }))) : (React.createElement(Tag_1.Tag, { padding: 20 },
                    React.createElement("ul", null,
                        React.createElement("li", null,
                            "Hover over a Function to learn more",
                            React.createElement("br", null),
                            React.createElement("br", null)),
                        React.createElement("li", null, "Select a Function to add to the Expression in the Editor")))))))) },
        React.createElement(SimpleButton_1.default, { "data-name": "expression-dropdown", icon: "arrow-down", iconPosition: 'end', mr: 1 },
            React.createElement(rebass_1.Flex, { marginRight: 1, fontSize: 2 },
                React.createElement(icons_1.Icon, { name: "equation" })))));
};
function BaseEditorInput(props) {
    const { expressionFunctions, testData, style, type } = props;
    const { ref: textAreaRefCallback, selectionStart, selectionEnd } = (0, useSelectionRange_1.useSelectionRange)();
    const cursor = selectionStart === selectionEnd ? selectionStart : null;
    let result;
    let evaluationError;
    let expressionError;
    let selectedFunctionName;
    const baseClassName = 'ab-ExpressionEditorInput';
    const buildParserExceptionMessage = (e) => {
        const parserExceptionSummary = 'Invalid expression is not parsable';
        if (!e.message) {
            return parserExceptionSummary;
        }
        const parserExceptionDetails = e.message;
        return (React.createElement("details", null,
            React.createElement(rebass_1.Flex, { marginBottom: 1, as: "summary" },
                parserExceptionSummary,
                React.createElement("i", null, " (click for more details)")),
            React.createElement(rebass_1.Text, { marginLeft: 3, style: { fontStyle: 'italic' } }, parserExceptionDetails)));
    };
    const testRowNode = (0, react_1.useMemo)(() => {
        const firstRowNode = props.api.gridApi.getFirstRowNode();
        if (!firstRowNode || !firstRowNode.data) {
            return {};
        }
        // clone the class instance to still keep the prototype methods
        return Object.assign(Object.create(Object.getPrototypeOf(firstRowNode)), firstRowNode);
    }, []);
    try {
        // explicitly parsing & evaluating the expression because we need full control of the resulted AST
        const expr = parser.parse(props.value || '');
        try {
            testRowNode.data = testData;
            // Mock datachangedevent for change-based functions e.g. PERCENT_CHANGE
            const dataChangedEvent = {
                newValue: 100,
                oldValue: 150,
            };
            result = expr.evaluate({
                // we need a fully-fledged rowNode as Adaptable accesses internal methods of it
                node: testRowNode,
                functions: expressionFunctions,
                evaluateCustomQueryVariable: props.api.internalApi.getQueryLanguageService().evaluateCustomQueryVariable,
                dataChangedEvent,
                ...props.api.internalApi.buildBaseContext(),
            });
        }
        catch (err) {
            if (err instanceof ExpressionEvaluationError_1.ExpressionEvaluationError) {
                evaluationError = err;
            }
            else {
                // unexpected error, pass it on to the enclosing handler
                throw err;
            }
        }
        const path = parser.findPathTo(expr.ast, cursor);
        selectedFunctionName = path[0] ? path[0].type : null;
    }
    catch (e) {
        // parse errors should have a hash, otherwise it's an unexpected runtime error
        const isParserException = !!e.hash;
        if (isParserException) {
            expressionError = buildParserExceptionMessage(e);
        }
        else {
            props.api.logWarn(`Unexpected error while evaluating '${props.value}':
      ${e}`);
        }
    }
    (0, react_1.useEffect)(() => {
        // update selected function only for focused textareas (where cursor is present)
        if (cursor != undefined) {
            props.onSelectedFunctionChange(selectedFunctionName ? expressionFunctions[selectedFunctionName] : null);
        }
    }, [selectedFunctionName]);
    const operatorButtons = props.editorButtons
        .filter((editorButtonDef) => !!expressionFunctions[editorButtonDef.functionName])
        .map((editorButtonDef) => (React.createElement(EditorButton_1.default, { key: `${editorButtonDef.functionName}-operator`, data: editorButtonDef.data, icon: editorButtonDef.icon }, !editorButtonDef.icon && (editorButtonDef.text || editorButtonDef.functionName))));
    const showDocumentationLink = props.api.internalApi.isDocumentationLinksDisplayed();
    return (React.createElement(React.Fragment, null,
        React.createElement(rebass_1.Flex, { className: baseClassName, "data-name": "expression-toolbar", py: 2, mb: 2, mt: 2, flexWrap: "wrap" },
            React.createElement(rebass_1.Flex, { style: { flex: 1, marginLeft: 5 }, flexWrap: "wrap" },
                React.createElement(FunctionsDropdown, { expressionFunctions: expressionFunctions, baseClassName: baseClassName }),
                operatorButtons),
            showDocumentationLink && (React.createElement(rebass_1.Flex, { alignItems: "flex-start" },
                React.createElement(ButtonInfo_1.ButtonInfo, { mr: 2, tooltip: 'Learn how to use the Expression Editor', onClick: () => window.open(DocumentationLinkConstants_1.ExpressionEditorDocsLink, '_blank') })))),
        React.createElement(Textarea_1.default, { "data-name": `expression-input-${type}`, ref: textAreaRefCallback, value: props.value || '', placeholder: props.placeholder || 'Create Query', disabled: props.disabled || false, className: (0, join_1.default)('ab-ExpressionEditor__textarea', // left for backwards compatibility
            `${baseClassName}__textarea`), autoFocus: true, spellCheck: "false", onChange: (event) => {
                props.onChange(event.target.value);
            }, style: style }),
        props.isFullExpression !== true && (React.createElement(HelpBlock_1.default, { mt: 2, mb: 2, p: 2, fontSize: 3 },
            "This Expression must resolve to a ",
            React.createElement("b", null, "boolean "),
            "(i.e. true / false) value")),
        expressionError && (React.createElement(ErrorBox_1.default, { width: "100%", style: { whiteSpace: 'pre-wrap' }, mt: 2 }, expressionError)),
        evaluationError && (React.createElement(ErrorBox_1.default, { style: { whiteSpace: 'pre-wrap' }, mt: 2 }, `${evaluationError.expressionFnName} ${evaluationError.message}`)),
        !props.hideResultPreview && result !== undefined && (React.createElement(rebass_1.Box, { className: `${baseClassName}__editor-feedback`, "data-name": "expression-editor-feedback", mt: 2, p: 2 },
            React.createElement("pre", { style: { whiteSpace: 'pre-wrap', margin: 0 } },
                "Result (using Test Data): ",
                React.createElement("b", null, JSON.stringify(result)))))));
}
exports.BaseEditorInput = BaseEditorInput;
