import * as parser from '../../parser/src';
import * as React from 'react';
import ErrorBox from '../ErrorBox';
import { useSelectionRange } from '../utils/useSelectionRange';
import EditorButton from './EditorButton';
import HelpBlock from '../HelpBlock';
import OverlayTrigger from '../OverlayTrigger';
import SimpleButton from '../SimpleButton';
import Textarea from '../Textarea';
import { useEffect, useMemo } from 'react';
import { ExpressionEvaluationError } from '../../parser/src/ExpressionEvaluationError';
import { ButtonInfo } from '../../View/Components/Buttons/ButtonInfo';
import { ExpressionEditorDocsLink } from '../../Utilities/Constants/DocumentationLinkConstants';
import { Icon } from '../icons';
import { useAdaptable } from '../../View/AdaptableContext';
import join from '../utils/join';
import { ExpressionFunctionDocumentation } from './ExpressionFunctionDocumentation';
import { Tag } from '../Tag';
import StringExtensions from '../../Utilities/Extensions/StringExtensions';
import Radio from '../Radio';
import { Box, Flex } from '../Flex';
import { AdaptableFormControlTextClear } from '../../View/Components/Forms/AdaptableFormControlTextClear';
import clsx from 'clsx';
const filterableCategories = [
    'dates',
    'logical',
    'maths',
    'strings',
    'changes',
    'comparison',
    'observable',
    'aggregation',
    'cumulative',
];
const getCategoryOrder = (category) => {
    const predefinedOrder = {
        special: 1,
        conditional: 2,
        logical: 3,
        comparison: 4,
        strings: 5,
        maths: 6,
        dates: 7,
        changes: 8,
    };
    return predefinedOrder[category] || 0;
};
const VarEditorButton = ({ className }) => {
    const adaptable = 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, { data: varString, key: varOption, className: className }, 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.Humanize(category),
            value: category,
        }));
        return [{ label: 'All', value: 'all' }, ...categoryOptions];
    }, [expressionFunctions]);
    const hidePopup = () => {
        dropdownRef.current.hide();
        setOverFunction(null);
    };
    const [searchInputValue, setSearchInputValue] = React.useState('');
    /**
     * Hide when:
     * - mouse leaves
     * - a function is inserted
     */
    return (React.createElement(OverlayTrigger, { ref: dropdownRef, showEvent: "mouseenter", hideEvent: "mouseleave", targetOffset: 5, render: () => (React.createElement(Flex, { className: `${baseClassName}__dropdown-functions-list-wrapper twa:max-w-[60vw] twa:shadow-md twa:bg-defaultbackground twa:text-text-on-defaultbackground twa:text-3 twa:rounded-standard twa:p-3`, flexDirection: "column", onMouseLeave: () => hidePopup() },
            React.createElement(Flex, { className: "twa:md:gap-3 twa:gap-2 twa:pb-2 twa:md:justify-between twa:justify-start twa:flex-wrap twa:border-b twa:border-primarydark/30 twa:mb-2" }, categoryOptions.map((option, index) => {
                return (React.createElement(Radio, { key: option.value, onFocus: (event) => {
                        event.preventDefault();
                        event.stopPropagation();
                    }, onClick: handleFunctionCategoryChange(option.value), checked: currentFunctionCategory === option.value }, option.label));
            })),
            React.createElement(Flex, { className: "twa:gap-2 twa:max-h-[60vh]", flexDirection: "row" },
                React.createElement(Flex, { flexDirection: "column", className: "twa:gap-2 twa:rounded-standard" },
                    React.createElement(Box, null,
                        React.createElement(AdaptableFormControlTextClear, { className: "twa:w-full", value: searchInputValue, OnTextChange: setSearchInputValue, placeholder: "Search Functions" })),
                    React.createElement(Flex, { className: `${baseClassName}__dropdown-functions-list twa:overflow-auto`, "data-name": "expression-dropdown-functions-list", flexDirection: "column" }, 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}()`;
                        };
                        const fns = functionsInGroup
                            .map((functionName) => {
                            if (searchInputValue &&
                                !functionName.toLowerCase().includes(searchInputValue.toLowerCase())) {
                                return null;
                            }
                            const buttonClassName = 'twa:mr-1 twa:hover:bg-accent twa:hover:text-accent-foreground';
                            return (React.createElement(Box, { key: functionName, onMouseEnter: () => setOverFunction(functionName), onClick: () => hidePopup() }, functionName === 'VAR' ? (React.createElement(VarEditorButton, { key: functionName, className: buttonClassName })) : (React.createElement(EditorButton, { data: getEditorButtonData(functionName), key: functionName, className: buttonClassName }, functionName))));
                        })
                            .filter(Boolean);
                        return (React.createElement(Box, { className: clsx('twa:mb-2 twa:mx-1', fns.length === 0 ? 'twa:hidden' : ''), key: groupName },
                            React.createElement(Tag, { className: "twa:mb-1 twa:bg-primarylight twa:text-text-on-primary twa:w-full" }, StringExtensions.Humanize(groupName)),
                            fns));
                    }))),
                React.createElement(Box, { className: clsx(`${baseClassName}__dropdown-functions-description `, `twa:flex-1 twa:p-2 twa:w-[600px] twa:max-w-[60vw]`, `twa:rounded-standard twa:shadow-sm twa:overflow-auto twa:h-auto`, 'twa:bg-primarylight twa:text-primary-foreground') }, overFunction ? (React.createElement(React.Fragment, null,
                    React.createElement(Tag, { className: "twa:bg-accent twa:text-accent-foreground" }, overFunction),
                    React.createElement(ExpressionFunctionDocumentation, { expressionFunction: expressionFunctions[overFunction] }))) : (React.createElement(Flex, { className: "twa:size-full twa:items-center twa:justify-center twa:text-2 twa:italic" },
                    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, "Click a Function to add it to the Expression in the Editor")))))))) },
        React.createElement(SimpleButton, { "data-name": "expression-dropdown", icon: "arrow-down", iconPosition: 'end', className: "twa:mr-1" },
            React.createElement(Flex, { className: "twa:mr-1 twa:text-2" },
                React.createElement(Icon, { name: "equation" })))));
};
export function BaseEditorInput(props) {
    const { expressionFunctions, testData, style, type } = props;
    const { ref: textAreaRefCallback, selectionStart, selectionEnd } = 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(Flex, { className: "twa:mb-1", as: "summary" },
                parserExceptionSummary,
                React.createElement("i", null, " (click for more details)")),
            React.createElement(Box, { className: "twa:ml-3 twa:italic" }, parserExceptionDetails)));
    };
    const testRowNode = 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) {
                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}`);
        }
    }
    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, { 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(Flex, { "data-name": "expression-toolbar", className: `${baseClassName} twa:py-2 twa:mb-2 twa:mt-2 twa:flex-wrap twa:w-full` },
            React.createElement(Flex, { className: "twa:flex-1 twa:ml-1 twa:flex-wrap" },
                React.createElement(FunctionsDropdown, { expressionFunctions: expressionFunctions, baseClassName: baseClassName }),
                operatorButtons),
            showDocumentationLink && (React.createElement(Flex, { alignItems: "flex-start" },
                React.createElement(ButtonInfo, { className: "twa:mr-2", tooltip: 'Learn how to use the Expression Editor', onClick: () => window.open(ExpressionEditorDocsLink, '_blank') })))),
        React.createElement(Textarea, { "data-name": `expression-input-${type}`, ref: textAreaRefCallback, value: props.value || '', placeholder: props.placeholder || 'Create Query', disabled: props.disabled || false, className: join('ab-ExpressionEditor__textarea', // left for backwards compatibility
            `${baseClassName}__textarea`, 'twa:w-full twa:min-h-[100px] twa:p-2 twa:text-4 twa:resize-y twa:font-mono'), autoFocus: true, spellCheck: "false", onChange: (event) => {
                props.onChange(event.target.value);
            }, style: style }),
        props.isFullExpression !== true && (React.createElement(HelpBlock, { className: "twa:my-2 twa:p-2 twa:text-3" },
            "This Expression must resolve to a ",
            React.createElement("b", null, "boolean "),
            "(i.e. true / false) value")),
        expressionError && (React.createElement(ErrorBox, { className: "twa:mt-2 twa:whitespace-pre-wrap twa:w-full" }, expressionError)),
        evaluationError && (React.createElement(ErrorBox, { className: "twa:mt-2 twa:whitespace-pre-wrap" }, `${evaluationError.expressionFnName} ${evaluationError.message}`)),
        !props.hideResultPreview && result !== undefined && (React.createElement(Box, { className: `${baseClassName}__editor-feedback twa:mt-2 twa:p-2 twa:rounded-standard`, "data-name": "expression-editor-feedback" },
            React.createElement("pre", { className: "twa:whitespace-pre-wrap twa:m-0" },
                "Result (using test data):",
                ' ',
                React.createElement("b", { className: "twa:bg-accentlight twa:text-accent twa:rounded-full twa:p-1 twa:px-2" }, JSON.stringify(result)))))));
}
