/**
 * Copyright (c) 2020-present, Goldman Sachs
 *
 * Licensed 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.
 */
import { action, computed, makeObservable, observable, flow } from 'mobx';
import { assertTrue, guaranteeNonNullable, guaranteeType, IllegalStateError, uuid, addUniqueEntry, deleteEntry, assertErrorThrown, filterByType, ActionState, hashArray, isNonNullable, } from '@finos/legend-shared';
import { QueryBuilderPropertyExpressionState } from '../QueryBuilderPropertyEditorState.js';
import { AbstractPropertyExpression, observe_ValueSpecification, CollectionInstanceValue, InstanceValue, SimpleFunctionExpression, matchFunctionName, FunctionExpression, Multiplicity, VariableExpression as VariableExpressionClass, } from '@finos/legend-graph';
import { DEFAULT_LAMBDA_VARIABLE_NAME } from '../QueryBuilderConfig.js';
import { buildPropertyTypeaheadQuery, buildTypeaheadOptions, performTypeahead, } from '../QueryBuilderTypeaheadHelper.js';
import { QUERY_BUILDER_GROUP_OPERATION } from '../QueryBuilderGroupOperationHelper.js';
import { QUERY_BUILDER_STATE_HASH_STRUCTURE } from '../QueryBuilderStateHashUtils.js';
import { getCollectionValueSpecificationType, getNonCollectionValueSpecificationType, isValidInstanceValue, isValueExpressionReferencedInValue, } from '../QueryBuilderValueSpecificationHelper.js';
import { instanceValue_setValues } from '../shared/ValueSpecificationModifierHelper.js';
import { QUERY_BUILDER_SUPPORTED_FUNCTIONS } from '../../graph/QueryBuilderMetaModelConst.js';
import { buildPropertyExpressionChain } from '../QueryBuilderValueSpecificationBuilderHelper.js';
export var QUERY_BUILDER_FILTER_DND_TYPE;
(function (QUERY_BUILDER_FILTER_DND_TYPE) {
    QUERY_BUILDER_FILTER_DND_TYPE["GROUP_CONDITION"] = "QUERY_BUILDER_FILTER_DND_TYPE.GROUP_CONDITION";
    QUERY_BUILDER_FILTER_DND_TYPE["CONDITION"] = "QUERY_BUILDER_FILTER_DND_TYPE.CONDITION";
    QUERY_BUILDER_FILTER_DND_TYPE["BLANK_CONDITION"] = "QUERY_BUILDER_FILTER_DND_TYPE.BLANK_CONDITION";
})(QUERY_BUILDER_FILTER_DND_TYPE || (QUERY_BUILDER_FILTER_DND_TYPE = {}));
export const isCollectionProperty = (propertyExpression) => {
    let currentExpression = propertyExpression;
    while (currentExpression instanceof AbstractPropertyExpression) {
        // Check if the property chain can results in column that have multiple values
        if (currentExpression.func.value.multiplicity.upperBound === undefined ||
            currentExpression.func.value.multiplicity.upperBound > 1) {
            return true;
        }
        currentExpression = currentExpression.parametersValues[0];
        // Take care of chains of subtype
        while (currentExpression instanceof SimpleFunctionExpression &&
            matchFunctionName(currentExpression.functionName, QUERY_BUILDER_SUPPORTED_FUNCTIONS.SUBTYPE)) {
            currentExpression = currentExpression.parametersValues[0];
        }
    }
    return false;
};
export class FilterConditionValueState {
    conditionState;
    constructor(conditionState) {
        this.conditionState = conditionState;
    }
    get type() {
        return undefined;
    }
    get isCollection() {
        return false;
    }
    get hashCode() {
        return hashArray([
            QUERY_BUILDER_STATE_HASH_STRUCTURE.FILTER_CONDITION_RIGHT_VALUE,
        ]);
    }
}
// This class is used to represent the value of a filter condition when the value is
// a ValueSpecification.
export class FilterValueSpecConditionValueState extends FilterConditionValueState {
    value;
    constructor(conditionState, value) {
        super(conditionState);
        makeObservable(this, {
            value: observable,
            setValue: action,
        });
        this.value = this.setValue(value);
    }
    get type() {
        if (this.value instanceof CollectionInstanceValue) {
            return getCollectionValueSpecificationType(this.conditionState.filterState.queryBuilderState.graphManagerState
                .graph, this.value.values);
        }
        return this.value
            ? getNonCollectionValueSpecificationType(this.value)
            : undefined;
    }
    setValue(val) {
        this.value = val
            ? observe_ValueSpecification(val, this.conditionState.filterState.queryBuilderState.observerContext)
            : undefined;
        return this.value;
    }
    get isCollection() {
        return this.value instanceof CollectionInstanceValue;
    }
    get hashCode() {
        return hashArray([
            QUERY_BUILDER_STATE_HASH_STRUCTURE.FILTER_CONDITION_RIGHT_VALUE_SPEC,
            this.value,
        ]);
    }
}
// This class is used to represent the value of a filter condition when the value is a
// PropertyExpressionState (which comes from DND a property node from the explorer tree
// or a column from the fetch structure panel).
export class FilterPropertyExpressionStateConditionValueState extends FilterConditionValueState {
    propertyExpressionState;
    constructor(conditionState, propertyExpressionState) {
        super(conditionState);
        makeObservable(this, {
            propertyExpressionState: observable,
            changePropertyExpressionState: action,
        });
        this.propertyExpressionState = propertyExpressionState;
    }
    get type() {
        return this.propertyExpressionState.propertyExpression.func.value
            .genericType.value.rawType;
    }
    get isCollection() {
        return isCollectionProperty(this.propertyExpressionState.propertyExpression);
    }
    changePropertyExpressionState(propertyExpressionState) {
        this.propertyExpressionState = propertyExpressionState;
    }
}
// This class is used to represent the value of a filter condition when the value is
// a relation column accessor (which comes from DND a relation column from the explorer
// tree or from the fetch structure panel). Used when the filter source is also a
// relation column, allowing expressions like `$x.colA == $x.colB`.
export class FilterRelationColumnConditionValueState extends FilterConditionValueState {
    columnName;
    columnType;
    columnMultiplicity;
    constructor(conditionState, columnName, columnType, columnMultiplicity) {
        super(conditionState);
        makeObservable(this, {
            columnName: observable,
            columnType: observable,
            columnMultiplicity: observable,
            changeColumn: action,
        });
        this.columnName = columnName;
        this.columnType = columnType;
        this.columnMultiplicity = columnMultiplicity;
    }
    get type() {
        return this.columnType;
    }
    get isCollection() {
        return (this.columnMultiplicity.upperBound === undefined ||
            this.columnMultiplicity.upperBound > 1);
    }
    changeColumn(columnName, columnType, columnMultiplicity) {
        this.columnName = columnName;
        this.columnType = columnType;
        this.columnMultiplicity = columnMultiplicity;
    }
    get hashCode() {
        return hashArray([
            QUERY_BUILDER_STATE_HASH_STRUCTURE.FILTER_CONDITION_RIGHT_VALUE_RELATION_COLUMN,
            this.columnName,
            this.columnMultiplicity.lowerBound,
            this.columnMultiplicity.upperBound ?? '',
        ]);
    }
}
/**
 * Abstract base class for the left-hand side (source) of a filter condition.
 * This allows filtering on different source types (class properties, relation columns)
 * without operators needing to know the specifics of the source.
 */
export class FilterConditionSourceState {
}
export class FilterPropertyExpressionSourceState extends FilterConditionSourceState {
    propertyExpressionState;
    constructor(propertyExpressionState) {
        super();
        this.propertyExpressionState = propertyExpressionState;
    }
    get type() {
        return this.propertyExpressionState.propertyExpression.func.value
            .genericType.value.rawType;
    }
    get label() {
        return this.propertyExpressionState.title;
    }
    get title() {
        return this.propertyExpressionState.title;
    }
    get isValid() {
        return this.propertyExpressionState.isValid;
    }
    get requiresExistsHandling() {
        return this.propertyExpressionState.requiresExistsHandling;
    }
    buildLeftExpression(queryBuilderState, lambdaParameterName) {
        return guaranteeNonNullable(buildPropertyExpressionChain(this.propertyExpressionState.propertyExpression, queryBuilderState, lambdaParameterName));
    }
    get hashCode() {
        return hashArray([
            QUERY_BUILDER_STATE_HASH_STRUCTURE.FILTER_CONDITION_SOURCE_PROPERTY_EXPRESSION,
            this.propertyExpressionState,
        ]);
    }
}
export class FilterRelationColumnSourceState extends FilterConditionSourceState {
    columnName;
    columnType;
    columnMultiplicity;
    constructor(columnName, columnType, columnMultiplicity) {
        super();
        this.columnName = columnName;
        this.columnType = columnType;
        this.columnMultiplicity = columnMultiplicity;
    }
    get multiplicity() {
        return this.columnMultiplicity;
    }
    get type() {
        return this.columnType;
    }
    get label() {
        return this.columnName;
    }
    get title() {
        return this.columnName;
    }
    get isValid() {
        return true;
    }
    get requiresExistsHandling() {
        return false;
    }
    buildLeftExpression(queryBuilderState, lambdaParameterName) {
        const relationType = queryBuilderState.sourceRelationType;
        if (relationType) {
            const col = guaranteeNonNullable(relationType.columns.find((c) => c.name === this.columnName), `Can't find column '${this.columnName}' in relation`);
            const funcExp = new FunctionExpression(col.name);
            funcExp.func = col;
            funcExp.parametersValues = [
                new VariableExpressionClass(lambdaParameterName, Multiplicity.ONE),
            ];
            return funcExp;
        }
        throw new IllegalStateError(`Can't build expression for relation column '${this.columnName}': no source relation type`);
    }
    get hashCode() {
        return hashArray([
            QUERY_BUILDER_STATE_HASH_STRUCTURE.FILTER_CONDITION_SOURCE_RELATION_COLUMN,
            this.columnName,
            this.columnMultiplicity.lowerBound,
            this.columnMultiplicity.upperBound ?? '',
        ]);
    }
}
export class FilterConditionState {
    filterState;
    sourceState;
    operator;
    rightConditionValue;
    existsLambdaParamNames = [];
    typeaheadSearchResults;
    typeaheadSearchState = ActionState.create();
    constructor(filterState, source, operator) {
        makeObservable(this, {
            sourceState: observable,
            operator: observable,
            rightConditionValue: observable,
            existsLambdaParamNames: observable,
            typeaheadSearchResults: observable,
            changeOperator: action,
            setOperator: action,
            setRightConditionValue: action,
            addExistsLambdaParamNames: action,
            buildRightConditionValueFromValueSpec: action,
            buildRightConditionValueFromPropertyExpressionState: action,
            buildRightConditionValueFromRelationColumn: action,
            handleTypeaheadSearch: flow,
            operators: computed,
            hashCode: computed,
        });
        this.filterState = filterState;
        if (source instanceof AbstractPropertyExpression) {
            this.sourceState = new FilterPropertyExpressionSourceState(new QueryBuilderPropertyExpressionState(filterState.queryBuilderState, source));
        }
        else {
            this.sourceState = source;
        }
        // operator
        if (operator) {
            this.operator = operator;
        }
        else {
            assertTrue(this.operators.length !== 0, `Can't find an operator for property '${this.sourceState.title}': no operators registered`);
            this.operator = this.operators[0];
        }
        this.buildRightConditionValueFromValueSpec(this.operator.getDefaultFilterConditionValue(this));
    }
    /**
     * Backward-compatible accessor for the property expression state.
     * Only valid when the source is a property expression.
     */
    get propertyExpressionState() {
        return guaranteeType(this.sourceState, FilterPropertyExpressionSourceState, `Filter condition source is not a property expression`).propertyExpressionState;
    }
    /**
     * The raw type of the left-hand side of the filter condition.
     */
    get leftConditionType() {
        return this.sourceState.type;
    }
    get operators() {
        return this.filterState.operators.filter((op) => op.isCompatibleWithFilterConditionProperty(this));
    }
    *handleTypeaheadSearch(searchValue) {
        try {
            this.typeaheadSearchState.inProgress();
            this.typeaheadSearchResults = undefined;
            const rightConditionValue = guaranteeType(this.rightConditionValue, FilterValueSpecConditionValueState);
            const value = searchValue ?? rightConditionValue.value;
            if (performTypeahead(value)) {
                const result = (yield this.filterState.queryBuilderState.graphManagerState.graphManager.runQuery(buildPropertyTypeaheadQuery(this.filterState.queryBuilderState, this.propertyExpressionState.propertyExpression, value), guaranteeNonNullable(this.filterState.queryBuilderState.executionContextState.mapping), guaranteeNonNullable(this.filterState.queryBuilderState.executionContextState
                    .runtimeValue), this.filterState.queryBuilderState.graphManagerState.graph, {
                    forceFromExpression: this.filterState.queryBuilderState.forceFromExpressionForExec,
                })).executionResult;
                this.typeaheadSearchResults = buildTypeaheadOptions(result);
            }
            this.typeaheadSearchState.pass();
        }
        catch (error) {
            assertErrorThrown(error);
            this.typeaheadSearchResults = [];
            this.typeaheadSearchState.fail();
        }
        finally {
            this.typeaheadSearchState.complete();
        }
    }
    changeOperator(val) {
        this.setOperator(val);
        if (!this.operator.isCompatibleWithFilterConditionValue(this)) {
            let defaultValue = this.operator.getDefaultFilterConditionValue(this);
            // Don't allow invalid InstanceValues or empty strings to be set as list element
            if (defaultValue instanceof CollectionInstanceValue &&
                this.rightConditionValue instanceof
                    FilterValueSpecConditionValueState &&
                this.rightConditionValue.value instanceof InstanceValue &&
                isValidInstanceValue(this.rightConditionValue.value) &&
                this.rightConditionValue.value.values[0] !== '') {
                instanceValue_setValues(defaultValue, [this.rightConditionValue.value], this.filterState.queryBuilderState.observerContext);
            }
            else if (defaultValue instanceof InstanceValue &&
                this.rightConditionValue instanceof
                    FilterValueSpecConditionValueState &&
                this.rightConditionValue.value instanceof CollectionInstanceValue &&
                this.rightConditionValue.value.values.length) {
                defaultValue = this.rightConditionValue.value.values[0];
            }
            this.buildRightConditionValueFromValueSpec(defaultValue);
        }
    }
    setOperator(val) {
        this.operator = val;
    }
    setRightConditionValue(val) {
        this.rightConditionValue = val;
    }
    addExistsLambdaParamNames(val) {
        this.existsLambdaParamNames.push(val);
    }
    buildRightConditionValueFromValueSpec(val) {
        if (this.rightConditionValue instanceof FilterValueSpecConditionValueState) {
            this.rightConditionValue.setValue(val);
        }
        else {
            this.setRightConditionValue(new FilterValueSpecConditionValueState(this, val));
        }
    }
    buildRightConditionValueFromPropertyExpressionState(propertyExpressionState) {
        if (this.rightConditionValue instanceof
            FilterPropertyExpressionStateConditionValueState) {
            this.rightConditionValue.changePropertyExpressionState(propertyExpressionState);
        }
        else {
            this.setRightConditionValue(new FilterPropertyExpressionStateConditionValueState(this, propertyExpressionState));
        }
    }
    buildRightConditionValueFromRelationColumn(columnName, columnType, columnMultiplicity) {
        if (this.rightConditionValue instanceof
            FilterRelationColumnConditionValueState) {
            this.rightConditionValue.changeColumn(columnName, columnType, columnMultiplicity);
        }
        else {
            this.setRightConditionValue(new FilterRelationColumnConditionValueState(this, columnName, columnType, columnMultiplicity));
        }
    }
    get hashCode() {
        return hashArray([
            QUERY_BUILDER_STATE_HASH_STRUCTURE.FILTER_CONDITION_STATE,
            this.sourceState,
            this.rightConditionValue ?? '',
            this.operator,
        ]);
    }
}
export class QueryBuilderFilterTreeNodeData {
    id = uuid();
    label = '';
    // NOTE: we don't use the `isSelected` attribute is not used since we keep track of it from the tree data level
    isOpen;
    parentId;
    constructor(parentId) {
        this.parentId = parentId;
        makeObservable(this, {
            isOpen: observable,
            parentId: observable,
            setIsOpen: action,
            setParentId: action,
            hashCode: computed,
        });
    }
    setIsOpen(val) {
        this.isOpen = val;
    }
    setParentId(val) {
        this.parentId = val;
    }
}
export class QueryBuilderFilterTreeOperationNodeData extends QueryBuilderFilterTreeNodeData {
    childrenIds = [];
    lambdaParameterName;
    constructor(parentId) {
        super(parentId);
        makeObservable(this, {
            childrenIds: observable,
            addChildNode: action,
            removeChildNode: action,
            dragPreviewLabel: computed,
        });
        this.isOpen = true;
    }
    addChildNode(node) {
        addUniqueEntry(this.childrenIds, node.id);
        node.setParentId(this.id);
    }
    removeChildNode(node) {
        deleteEntry(this.childrenIds, node.id);
        node.setParentId(undefined);
    }
    addChildNodeAt(node, idx) {
        if (!this.childrenIds.find((childId) => childId === node.id)) {
            idx = Math.max(0, Math.min(idx, this.childrenIds.length - 1));
            this.childrenIds.splice(idx, 0, node.id);
            node.setParentId(this.id);
        }
    }
}
export class QueryBuilderFilterTreeGroupNodeData extends QueryBuilderFilterTreeOperationNodeData {
    groupOperation;
    constructor(parentId, groupOperation) {
        super(parentId);
        makeObservable(this, {
            groupOperation: observable,
            setGroupOperation: action,
        });
        this.groupOperation = groupOperation;
        this.isOpen = true;
    }
    get dragPreviewLabel() {
        return `${this.groupOperation.toUpperCase()} group`;
    }
    setGroupOperation(val) {
        this.groupOperation = val;
    }
    get hashCode() {
        return hashArray([
            QUERY_BUILDER_STATE_HASH_STRUCTURE.FILTER_TREE_GROUP_NODE_DATA,
            this.parentId ?? '',
            hashArray(this.childrenIds),
            this.groupOperation,
            this.lambdaParameterName ?? '',
        ]);
    }
}
export class QueryBuilderFilterTreeExistsNodeData extends QueryBuilderFilterTreeOperationNodeData {
    filterState;
    propertyExpressionState;
    constructor(filterState, parentId) {
        super(parentId);
        makeObservable(this, {
            propertyExpressionState: observable,
            setPropertyExpression: action,
        });
        this.filterState = filterState;
        this.isOpen = true;
    }
    get dragPreviewLabel() {
        return `exists`;
    }
    setPropertyExpression(val) {
        this.propertyExpressionState = new QueryBuilderPropertyExpressionState(this.filterState.queryBuilderState, val);
    }
    get hashCode() {
        return hashArray([
            QUERY_BUILDER_STATE_HASH_STRUCTURE.FILTER_TREE_EXISTS_NODE_DATA,
            this.parentId ?? '',
            hashArray(this.childrenIds),
            this.propertyExpressionState.propertyExpression,
            this.lambdaParameterName ?? '',
        ]);
    }
}
export const isExistsNodeChild = (node) => {
    let parentNode = node.condition.filterState.getParentNode(node);
    while (parentNode !== undefined) {
        if (parentNode instanceof QueryBuilderFilterTreeExistsNodeData) {
            return true;
        }
        parentNode = node.condition.filterState.getParentNode(parentNode);
    }
    return false;
};
/**
 *
 * @param node the node from which to start searching
 * @returns the furthest away parent node that is an exists node, or
 *          undefined if none exists.
 */
export const getFurthestExistsNodeParent = (node) => {
    let furthestExistsNode = undefined;
    let parentNode = node.condition.filterState.getParentNode(node);
    while (parentNode) {
        if (parentNode instanceof QueryBuilderFilterTreeExistsNodeData) {
            furthestExistsNode = parentNode;
        }
        parentNode = node.condition.filterState.getParentNode(parentNode);
    }
    return furthestExistsNode;
};
export class QueryBuilderFilterTreeConditionNodeData extends QueryBuilderFilterTreeNodeData {
    condition;
    isNewlyAdded;
    constructor(parentId, condition) {
        super(parentId);
        makeObservable(this, {
            condition: observable,
            isNewlyAdded: observable,
            setIsNewlyAdded: action,
            dragPreviewLabel: computed,
        });
        this.isNewlyAdded = false;
        this.condition = condition;
    }
    setIsNewlyAdded(val) {
        this.isNewlyAdded = val;
    }
    get dragPreviewLabel() {
        return this.condition.sourceState.title;
    }
    get hashCode() {
        return hashArray([
            QUERY_BUILDER_STATE_HASH_STRUCTURE.FILTER_TREE_CONDIITION_NODE_DATA,
            this.parentId ?? '',
            this.condition,
        ]);
    }
}
export class QueryBuilderFilterTreeBlankConditionNodeData extends QueryBuilderFilterTreeNodeData {
    constructor(parentId) {
        super(parentId);
        makeObservable(this, {
            dragPreviewLabel: computed,
        });
    }
    get dragPreviewLabel() {
        return '<blank>';
    }
    get hashCode() {
        return hashArray([
            QUERY_BUILDER_STATE_HASH_STRUCTURE.FILTER_TREE_BLANK_CONDITION_NODE_DATA,
            this.parentId ?? '',
        ]);
    }
}
export class QueryBuilderFilterState {
    queryBuilderState;
    lambdaParameterName = DEFAULT_LAMBDA_VARIABLE_NAME;
    rootIds = [];
    nodes = new Map();
    selectedNode;
    isRearrangingConditions = false;
    operators = [];
    showPanel = true;
    constructor(queryBuilderState, operators) {
        makeObservable(this, {
            rootIds: observable,
            nodes: observable,
            selectedNode: observable,
            isRearrangingConditions: observable,
            lambdaParameterName: observable,
            showPanel: observable,
            setLambdaParameterName: action,
            setRearrangingConditions: action,
            setSelectedNode: action,
            addNodeFromNode: action,
            replaceBlankNodeWithNode: action,
            addGroupConditionNodeFromNode: action,
            newGroupConditionFromNode: action,
            newGroupWithConditionFromNode: action,
            removeNodeAndPruneBranch: action,
            pruneTree: action,
            simplifyTree: action,
            collapseTree: action,
            setShowPanel: action,
            expandTree: action,
            allValidationIssues: computed,
            hasInvalidFilterValues: computed,
            hasInvalidDerivedPropertyParameters: computed,
            hashCode: computed,
        });
        this.queryBuilderState = queryBuilderState;
        this.operators = operators;
    }
    get isEmpty() {
        return !this.nodes.size && !this.rootIds.length;
    }
    setLambdaParameterName(val) {
        this.lambdaParameterName = val;
    }
    setShowPanel(val) {
        this.showPanel = val;
    }
    setRearrangingConditions(val) {
        this.isRearrangingConditions = val;
    }
    setSelectedNode(val) {
        this.selectedNode = val;
    }
    getNode(id) {
        return guaranteeNonNullable(this.nodes.get(id), `Can't find query builder filter tree node with ID '${id}'`);
    }
    getRootNode() {
        assertTrue(this.rootIds.length < 2, 'Query builder filter tree cannot have more than 1 root');
        const rootId = this.rootIds[0];
        return rootId ? this.getNode(rootId) : undefined;
    }
    getParentNode(node) {
        return node.parentId
            ? guaranteeType(this.nodes.get(node.parentId), QueryBuilderFilterTreeOperationNodeData)
            : undefined;
    }
    addRootNode(node) {
        const rootNode = this.getRootNode();
        this.nodes.set(node.id, node);
        if (rootNode instanceof QueryBuilderFilterTreeGroupNodeData) {
            rootNode.addChildNode(node);
        }
        else if (rootNode instanceof QueryBuilderFilterTreeConditionNodeData ||
            rootNode instanceof QueryBuilderFilterTreeBlankConditionNodeData ||
            rootNode instanceof QueryBuilderFilterTreeExistsNodeData) {
            // if the root node is condition node, form a group between the root node and the new node and nominate the group node as the new root
            const groupNode = new QueryBuilderFilterTreeGroupNodeData(undefined, QUERY_BUILDER_GROUP_OPERATION.AND);
            groupNode.addChildNode(rootNode);
            groupNode.addChildNode(node);
            this.rootIds = [groupNode.id];
            this.nodes.set(groupNode.id, groupNode);
        }
        else if (!rootNode) {
            // if there is no root node, set this node as the root
            this.rootIds = [node.id];
        }
    }
    addNodeFromNode(node, fromNode) {
        if (fromNode instanceof QueryBuilderFilterTreeGroupNodeData) {
            this.nodes.set(node.id, node);
            fromNode.addChildNode(node);
        }
        else if (fromNode instanceof QueryBuilderFilterTreeExistsNodeData) {
            // Here we check if there are any child nodes for exists node. The rationale
            // behind doing this check is if there are no childs we can just add a child
            // node to this otherwise we need to add a group condition for the existing
            // child node and the node we are trying to add.
            if (!fromNode.childrenIds.length) {
                this.nodes.set(node.id, node);
                fromNode.addChildNode(node);
            }
            else {
                this.nodes.set(node.id, node);
                const groupNode = new QueryBuilderFilterTreeGroupNodeData(undefined, QUERY_BUILDER_GROUP_OPERATION.AND);
                groupNode.addChildNode(guaranteeNonNullable(this.nodes.get(guaranteeNonNullable(fromNode.childrenIds[0]))));
                groupNode.addChildNode(node);
                groupNode.lambdaParameterName = fromNode.lambdaParameterName;
                this.nodes.set(groupNode.id, groupNode);
                fromNode.childrenIds = [];
                fromNode.addChildNode(groupNode);
            }
        }
        else if (fromNode instanceof QueryBuilderFilterTreeConditionNodeData ||
            fromNode instanceof QueryBuilderFilterTreeBlankConditionNodeData) {
            this.nodes.set(node.id, node);
            const fromNodeParent = this.getParentNode(fromNode);
            if (fromNodeParent) {
                fromNodeParent.addChildNode(node);
            }
            else {
                this.addRootNode(node);
            }
        }
        else if (!this.selectedNode) {
            // if no current node is selected, the node will be added to root
            this.addRootNode(node);
        }
    }
    replaceBlankNodeWithNode(node, blankNode) {
        this.nodes.set(node.id, node);
        const blankNodeParent = this.getParentNode(blankNode);
        if (blankNodeParent) {
            const blankNodeIdx = blankNodeParent.childrenIds.findIndex((childId) => childId === blankNode.id);
            blankNodeParent.addChildNodeAt(node, blankNodeIdx);
            blankNodeParent.removeChildNode(blankNode);
        }
        else {
            this.addRootNode(node);
        }
        this.removeNode(blankNode);
    }
    addGroupConditionNodeFromNode(fromNode) {
        const newGroupNode = new QueryBuilderFilterTreeGroupNodeData(undefined, QUERY_BUILDER_GROUP_OPERATION.AND);
        const newBlankConditionNode1 = new QueryBuilderFilterTreeBlankConditionNodeData(undefined);
        const newBlankConditionNode2 = new QueryBuilderFilterTreeBlankConditionNodeData(undefined);
        this.nodes.set(newBlankConditionNode1.id, newBlankConditionNode1);
        this.nodes.set(newBlankConditionNode2.id, newBlankConditionNode2);
        newGroupNode.addChildNode(newBlankConditionNode1);
        newGroupNode.addChildNode(newBlankConditionNode2);
        this.addNodeFromNode(newGroupNode, fromNode);
    }
    /**
     *
     * Function to create group condition from node where either of the
     * child nodes of group condition is `exists` node
     */
    newGroupConditionFromNode(fromNode, node, operation) {
        const fromNodeParent = this.getParentNode(fromNode);
        const newGroupNode = new QueryBuilderFilterTreeGroupNodeData(undefined, operation ?? QUERY_BUILDER_GROUP_OPERATION.AND);
        this.nodes.set(newGroupNode.id, newGroupNode);
        fromNodeParent?.removeChildNode(fromNode);
        newGroupNode.addChildNode(fromNode);
        if (node) {
            this.nodes.set(node.id, node);
            newGroupNode.addChildNode(node);
        }
        newGroupNode.lambdaParameterName =
            fromNodeParent?.lambdaParameterName ?? this.lambdaParameterName;
        if (fromNodeParent) {
            fromNodeParent.addChildNode(newGroupNode);
        }
        else {
            deleteEntry(this.rootIds, fromNode.id);
            this.addRootNode(newGroupNode);
        }
        return newGroupNode;
    }
    newGroupWithConditionFromNode(node, fromNode) {
        const newNode = node ?? new QueryBuilderFilterTreeBlankConditionNodeData(undefined);
        if (fromNode instanceof QueryBuilderFilterTreeConditionNodeData) {
            const fromNodeParent = this.getParentNode(fromNode);
            if (fromNodeParent) {
                const fromNodeIdx = fromNodeParent.childrenIds.findIndex((childId) => childId === fromNode.id);
                fromNodeParent.removeChildNode(fromNode);
                const newGroupNode = new QueryBuilderFilterTreeGroupNodeData(undefined, QUERY_BUILDER_GROUP_OPERATION.AND);
                this.nodes.set(newNode.id, newNode);
                this.nodes.set(newGroupNode.id, newGroupNode);
                newGroupNode.addChildNode(fromNode);
                newGroupNode.addChildNode(newNode);
                newGroupNode.lambdaParameterName = fromNodeParent.lambdaParameterName;
                fromNodeParent.addChildNodeAt(newGroupNode, fromNodeIdx);
            }
            else {
                this.addRootNode(newNode);
            }
        }
    }
    removeNode(node) {
        this.nodes.delete(node.id);
        // remove relationship with children nodes
        if (node instanceof QueryBuilderFilterTreeOperationNodeData) {
            // NOTE: we are deleting child node, i.e. modifying `childrenIds` as we iterate
            [...node.childrenIds].forEach((childId) => node.removeChildNode(this.getNode(childId)));
        }
        // remove relationship with parent node
        const parentNode = this.getParentNode(node);
        if (parentNode) {
            parentNode.removeChildNode(node);
        }
        else {
            deleteEntry(this.rootIds, node.id);
        }
    }
    pruneChildlessGroupNodes() {
        const getChildlessGroupNodes = () => Array.from(this.nodes.values())
            .filter(filterByType(QueryBuilderFilterTreeGroupNodeData))
            .filter((node) => !node.childrenIds.length);
        let nodesToProcess = getChildlessGroupNodes();
        while (nodesToProcess.length) {
            nodesToProcess.forEach((node) => this.removeNode(node));
            nodesToProcess = getChildlessGroupNodes();
        }
    }
    pruneOrphanNodes() {
        const getOrphanNodes = () => Array.from(this.nodes.values()).filter((node) => !node.parentId && !this.rootIds.includes(node.id)); // nodes without parent, except for root nodes
        let nodesToProcess = getOrphanNodes();
        while (nodesToProcess.length) {
            nodesToProcess.forEach((node) => this.removeNode(node));
            nodesToProcess = getOrphanNodes();
        }
    }
    /**
     * If group node has fewer than 2 children, flatten it
     */
    squashGroupNode(node) {
        if (node.childrenIds.length < 2) {
            const parentNode = this.getParentNode(node);
            // NOTE: we are deleting child node, i.e. modifying `childrenIds` as we iterate
            [...node.childrenIds].forEach((childId) => {
                const childNode = this.getNode(childId);
                node.removeChildNode(childNode);
                if (parentNode) {
                    parentNode.addChildNode(childNode);
                }
                else {
                    addUniqueEntry(this.rootIds, childId);
                }
            });
            // remove the group node
            this.nodes.delete(node.id);
            if (parentNode) {
                parentNode.removeChildNode(node);
            }
            else {
                deleteEntry(this.rootIds, node.id);
            }
        }
    }
    removeNodeAndPruneBranch(node) {
        const parentNode = this.getParentNode(node);
        this.removeNode(node);
        // squash parent node after the current node is deleted
        if (parentNode) {
            parentNode.removeChildNode(node);
            let currentParentNode = parentNode;
            while (currentParentNode &&
                currentParentNode instanceof QueryBuilderFilterTreeGroupNodeData) {
                if (currentParentNode.childrenIds.length >= 2) {
                    break;
                }
                this.squashGroupNode(currentParentNode);
                currentParentNode = this.getParentNode(currentParentNode);
            }
        }
        else {
            deleteEntry(this.rootIds, node.id);
        }
        this.pruneOrphanNodes();
        // check if selected node is still around, if not, unset the selected node
        if (this.selectedNode && !this.nodes.get(this.selectedNode.id)) {
            this.setSelectedNode(undefined);
        }
    }
    pruneTree() {
        this.setSelectedNode(undefined);
        // remove all blank nodes
        Array.from(this.nodes.values())
            .filter((node) => node instanceof QueryBuilderFilterTreeBlankConditionNodeData)
            .forEach((node) => this.removeNode(node));
        // prune
        this.pruneOrphanNodes();
        this.pruneChildlessGroupNodes();
        // squash group nodes
        // NOTE: since we have pruned all blank nodes and childless group nodes, at this point, if there are group nodes to be squashed
        // it will be group node with exactly 1 non-blank condition
        const getSquashableGroupNodes = () => Array.from(this.nodes.values())
            .filter(filterByType(QueryBuilderFilterTreeGroupNodeData))
            .filter((node) => node.childrenIds.length < 2)
            .filter((node) => {
            if (!node.childrenIds.length) {
                throw new IllegalStateError('Query builder filter tree found unexpected childless group nodes');
            }
            const firstChildNodeId = node.childrenIds[0];
            const childNode = this.getNode(firstChildNodeId);
            if (childNode instanceof QueryBuilderFilterTreeBlankConditionNodeData) {
                throw new IllegalStateError('Query builder filter tree found unexpected blank nodes');
            }
            return (this.getNode(firstChildNodeId) instanceof
                QueryBuilderFilterTreeConditionNodeData);
        });
        let nodesToProcess = getSquashableGroupNodes();
        while (nodesToProcess.length) {
            nodesToProcess.forEach((node) => this.squashGroupNode(node));
            nodesToProcess = getSquashableGroupNodes();
        }
        // check if selected node is still around, if not, unset the selected node
        if (this.selectedNode && !this.nodes.get(this.selectedNode.id)) {
            this.setSelectedNode(undefined);
        }
    }
    /**
     * Cleanup unecessary group nodes (i.e. group node whose group operation is the same as its parent's)
     */
    simplifyTree() {
        this.setSelectedNode(undefined);
        const getUnnecessaryNodes = () => Array.from(this.nodes.values())
            .filter(filterByType(QueryBuilderFilterTreeGroupNodeData))
            .filter((node) => {
            if (!node.parentId || !this.nodes.has(node.parentId)) {
                return false;
            }
            const parentGroupNode = this.nodes.get(node.parentId);
            return (parentGroupNode instanceof QueryBuilderFilterTreeGroupNodeData &&
                parentGroupNode.groupOperation === node.groupOperation);
        });
        // Squash these unnecessary group nodes
        let nodesToProcess = getUnnecessaryNodes();
        while (nodesToProcess.length) {
            nodesToProcess.forEach((node) => {
                const parentNode = guaranteeType(this.nodes.get(guaranteeNonNullable(node.parentId)), QueryBuilderFilterTreeGroupNodeData);
                // send all children of the current group node to their grandparent node
                [...node.childrenIds].forEach((childId) => {
                    const childNode = this.getNode(childId);
                    parentNode.addChildNode(childNode);
                });
                // remove the current group node
                parentNode.removeChildNode(node);
                // remove the node
                this.nodes.delete(node.id);
            });
            nodesToProcess = getUnnecessaryNodes();
        }
    }
    isValidMove(node, toNode) {
        const isMovingToItself = node === toNode;
        // disallow moving a node to its descendants
        let isMovingToChildNode = false;
        let currentParentNode = this.getParentNode(toNode);
        while (currentParentNode) {
            if (currentParentNode === node) {
                isMovingToChildNode = true;
                break;
            }
            currentParentNode = this.getParentNode(currentParentNode);
        }
        return !isMovingToItself && !isMovingToChildNode;
    }
    moveNode(node, toNode) {
        // do something;
    }
    collapseTree() {
        Array.from(this.nodes.values()).forEach((node) => node.setIsOpen(false));
    }
    expandTree() {
        Array.from(this.nodes.values()).forEach((node) => node.setIsOpen(true));
    }
    isVariableUsed(variable) {
        return Boolean(Array.from(this.nodes.values())
            .filter(filterByType(QueryBuilderFilterTreeConditionNodeData))
            .map((node) => node.condition.rightConditionValue instanceof
            FilterValueSpecConditionValueState
            ? node.condition.rightConditionValue.value
            : undefined)
            .filter(isNonNullable)
            .find((value) => isValueExpressionReferencedInValue(variable, value)));
    }
    isInvalidFilterPropertyExpressionState(node) {
        return (node instanceof QueryBuilderFilterTreeConditionNodeData &&
            !node.condition.sourceState.isValid);
    }
    isInvalidValueSpecFilterValue(node) {
        return (node instanceof QueryBuilderFilterTreeConditionNodeData &&
            node.condition.rightConditionValue instanceof
                FilterValueSpecConditionValueState &&
            node.condition.rightConditionValue.value instanceof InstanceValue &&
            !isValidInstanceValue(node.condition.rightConditionValue.value));
    }
    isInvalidPropertyExpressionStateFilterValue(node) {
        return (node instanceof QueryBuilderFilterTreeConditionNodeData &&
            node.condition.rightConditionValue instanceof
                FilterPropertyExpressionStateConditionValueState &&
            !node.condition.rightConditionValue.propertyExpressionState.isValid);
    }
    get allValidationIssues() {
        const validationIssues = [];
        Array.from(this.nodes.values()).forEach((node) => {
            if (node instanceof QueryBuilderFilterTreeConditionNodeData) {
                if (this.isInvalidValueSpecFilterValue(node)) {
                    validationIssues.push(`Filter value for ${node.condition.sourceState.title} is missing or invalid`);
                }
                if (this.isInvalidFilterPropertyExpressionState(node)) {
                    validationIssues.push(`Derived property parameter value for ${node.condition.sourceState.title} is missing or invalid`);
                }
                if (node.condition.rightConditionValue instanceof
                    FilterPropertyExpressionStateConditionValueState &&
                    this.isInvalidPropertyExpressionStateFilterValue(node)) {
                    validationIssues.push(`Derived property parameter value for ${node.condition.rightConditionValue.propertyExpressionState.title} is missing or invalid`);
                }
            }
        });
        return validationIssues;
    }
    get hasInvalidFilterValues() {
        return Array.from(this.nodes.values()).some((node) => this.isInvalidValueSpecFilterValue(node));
    }
    get hasInvalidDerivedPropertyParameters() {
        return Array.from(this.nodes.values()).some((node) => this.isInvalidFilterPropertyExpressionState(node) ||
            this.isInvalidPropertyExpressionStateFilterValue(node));
    }
    get hashCode() {
        return hashArray([
            QUERY_BUILDER_STATE_HASH_STRUCTURE.FILTER_STATE,
            hashArray(this.rootIds),
            hashArray(Array.from(this.nodes.values())),
        ]);
    }
}
//# sourceMappingURL=QueryBuilderFilterState.js.map