/**
 * 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 { flowResult, observable, action, reaction, flow, makeObservable, isComputedProp, } from 'mobx';
import { LEGEND_STUDIO_APP_EVENT } from '../../__lib__/LegendStudioEvent.js';
import { LogEvent, IllegalStateError, shallowStringify, noop, assertErrorThrown, hashObject, promisify, ActionState, guaranteeNonNullable, guaranteeType, } from '@finos/legend-shared';
import { EntityChangeConflict, EntityChangeType, EntityDiff, } from '@finos/legend-server-sdlc';
import { ObserverContext, observe_Graph, observe_GraphElements, } from '@finos/legend-graph';
import { keepAlive } from 'mobx-utils';
import { TextLocalChangesState } from './sidebar-state/LocalChangesState.js';
class RevisionChangeDetectionState {
    editorStore;
    graphState;
    changes = [];
    entityHashesIndex = new Map();
    isBuildingEntityHashesIndex = false;
    entities = [];
    currentEntityHashesIndex = new Map();
    setEntityHashesIndex(hashesIndex) {
        this.entityHashesIndex = hashesIndex;
    }
    setIsBuildingEntityHashesIndex(building) {
        this.isBuildingEntityHashesIndex = building;
    }
    setEntities(entities) {
        this.entities = entities;
    }
    constructor(editorStore, graphState) {
        makeObservable(this, {
            changes: observable.ref,
            entityHashesIndex: observable.ref,
            entities: observable.ref,
            isBuildingEntityHashesIndex: observable,
            setEntityHashesIndex: action,
            setIsBuildingEntityHashesIndex: action,
            setEntities: action,
            computeChanges: flow,
            computeChangesInTextMode: flow,
            buildEntityHashesIndex: flow,
        });
        this.editorStore = editorStore;
        this.graphState = graphState;
    }
    *computeChanges(quiet) {
        const startTime = Date.now();
        let changes = [];
        if (!this.isBuildingEntityHashesIndex) {
            const originalPaths = new Set(Array.from(this.entityHashesIndex.keys()));
            if (this.editorStore.graphManagerState.graph.allOwnElements.length) {
                yield Promise.all(this.editorStore.graphManagerState.graph.allOwnElements.map((element) => promisify(() => {
                    const elementPath = element.path;
                    const originalElementHash = this.entityHashesIndex.get(elementPath);
                    if (!originalElementHash) {
                        changes.push(new EntityDiff(undefined, elementPath, EntityChangeType.CREATE));
                    }
                    else if (originalElementHash !== element.hashCode) {
                        changes.push(new EntityDiff(elementPath, elementPath, EntityChangeType.MODIFY));
                    }
                    originalPaths.delete(elementPath);
                })));
            }
            changes = changes.concat(Array.from(originalPaths).map((path) => new EntityDiff(path, undefined, EntityChangeType.DELETE)));
        }
        this.changes = changes;
        if (!quiet) {
            this.editorStore.applicationStore.logService.info(LogEvent.create(LEGEND_STUDIO_APP_EVENT.CHANGE_DETECTION_COMPUTE_CHANGES__SUCCESS), Date.now() - startTime, 'ms');
        }
    }
    *computeChangesInTextMode(currentEntities, quiet) {
        const startTime = Date.now();
        const changes = [];
        const entityChanges = [];
        if (!this.isBuildingEntityHashesIndex) {
            let currentHashesIndex;
            if (currentEntities.length) {
                currentHashesIndex =
                    (yield this.editorStore.graphManagerState.graphManager.buildHashesIndex(currentEntities));
                this.currentEntityHashesIndex = currentHashesIndex;
            }
            const originalPaths = new Set(Array.from(this.entityHashesIndex.keys()));
            if (currentHashesIndex) {
                yield Promise.all(Array.from(currentHashesIndex.entries()).map(([elementPath, elementHash]) => promisify(() => {
                    const entity = currentEntities.find((e) => e.path === elementPath);
                    const originalElementHash = this.entityHashesIndex.get(elementPath);
                    if (!originalElementHash) {
                        changes.push(new EntityDiff(undefined, elementPath, EntityChangeType.CREATE));
                        entityChanges.push({
                            classifierPath: guaranteeNonNullable(entity).classifierPath,
                            entityPath: guaranteeNonNullable(entity).path,
                            content: guaranteeNonNullable(entity).content,
                            type: EntityChangeType.CREATE,
                        });
                    }
                    else if (originalElementHash !== elementHash) {
                        changes.push(new EntityDiff(elementPath, elementPath, EntityChangeType.MODIFY));
                        entityChanges.push({
                            classifierPath: guaranteeNonNullable(entity).classifierPath,
                            entityPath: guaranteeNonNullable(entity).path,
                            content: guaranteeNonNullable(entity).content,
                            type: EntityChangeType.MODIFY,
                        });
                    }
                    originalPaths.delete(elementPath);
                })));
            }
            yield promisify(() => {
                Array.from(originalPaths).forEach((path) => {
                    changes.push(new EntityDiff(path, undefined, EntityChangeType.DELETE));
                    entityChanges.push({
                        type: EntityChangeType.DELETE,
                        entityPath: path,
                    });
                });
            });
        }
        this.changes = changes;
        guaranteeType(this.editorStore.localChangesState, TextLocalChangesState).localChanges = entityChanges;
        if (!quiet) {
            this.editorStore.applicationStore.logService.info(LogEvent.create(LEGEND_STUDIO_APP_EVENT.CHANGE_DETECTION_COMPUTE_CHANGES__SUCCESS), Date.now() - startTime, 'ms');
        }
    }
    *buildEntityHashesIndex(entities, logEvent, quiet) {
        if (!this.entities.length && !this.entityHashesIndex.size) {
            return;
        }
        const startTime = Date.now();
        this.setIsBuildingEntityHashesIndex(true);
        try {
            const hashesIndex = (yield this.editorStore.graphManagerState.graphManager.buildHashesIndex(entities));
            this.setEntityHashesIndex(hashesIndex);
            this.setIsBuildingEntityHashesIndex(false);
            if (!quiet) {
                this.editorStore.applicationStore.logService.info(logEvent, '[ASYNC]', Date.now() - startTime, 'ms');
            }
        }
        catch (error) {
            assertErrorThrown(error);
            this.editorStore.applicationStore.logService.error(LogEvent.create(LEGEND_STUDIO_APP_EVENT.CHANGE_DETECTION__FAILURE), `Can't build hashes index`);
            this.setEntityHashesIndex(new Map());
            this.setIsBuildingEntityHashesIndex(false);
            throw new IllegalStateError(error);
        }
        finally {
            this.setIsBuildingEntityHashesIndex(false);
        }
    }
}
/**
 * In the SDLC flow of the app, there are several important revision that we want to keep track of. See diagram below:
 *
 *    (1. PJL)
 *     |
 *     |
 *    (2. CRB) ------- (3. CRH) ------ (4. CRL)
 *     |
 *     |
 *    (5. WSB) ------- (6. WSH) ------ (7. WSL)
 *     |
 *    ... (earlier revisions in the project)
 *
 * Annotations:
 * 1. PJL: Project HEAD revision
 * 2. CRB: Conflict resolution BASE revision
 * 3. CRH: Conflict resolution HEAD revision
 * 4. CRL: Conflict resolution LIVE revision (i.e. local graph state in conflict resolution mode)
 * 5. WSB: Workspace BASE revision
 * 6. WSH: Workspace HEAD revision
 * 7. WSL: Workspace LIVE revision (i.e. local graph state in standard mode)
 */
export class ChangeDetectionState {
    editorStore;
    graphState;
    graphObserveState = ActionState.create();
    initState = ActionState.create();
    /**
     * Keep the list of disposers to deactivate `keepAlive` for computed value of element hash code.
     * See {@link preComputeGraphElementHashes} for more details
     */
    graphElementHashCodeKeepAliveComputationDisposers = [];
    changeDetectionReaction;
    /**
     * [1. PJL] Store the entities from project HEAD (i.e. project latest revision)
     * This can be used to compute changes for a review as well as changes and potential conflicts when updating workspace
     */
    projectLatestRevisionState;
    /**
     * [2. CRB] Store the entities from the BASE revision of workspace with conflict resolution (this is different from the current workspace the user is on)
     * NOTE: the flow for conflict resolution is briefly like this (assume current user workspace is `w1`):
     * 1. When the user chooses to update workspace `w1`, the backend will compute changes between `w1` HEAD and `w1` BASE
     * 2. Create a new conflict resolution branch on top of project HEAD
     * 3. Apply the changes on this branch.
     *
     * So we now have 2 branchs normal `w1` and `w1` in conflict resolution. From the user perspective, they might not need to know this
     * So in the app, we have to check for the existence of the conflict resolution branch and make it supercede the original `w1` branch
     *
     * This branch, thus, will be used to show the users all the changes they have on top of conflict resolution BASE revision
     * during conflict resolution stage
     */
    conflictResolutionBaseRevisionState;
    /**
     * [3. CRH] Store the entities from the conflict resolution HEAD revision
     * This is used for computing the live diff, so that when we mark conflicts as resolved and accept conflict resolution
     * we can compute the entity changes
     */
    conflictResolutionHeadRevisionState;
    /**
     * [5. WSB] Store the entities from workspace BASE revision
     * This can be used for conflict resolution
     */
    workspaceBaseRevisionState;
    /**
     * [6. WSH] Store the entities from LOCAL workspace HEAD revision.
     * This can be used for computing local changes/live diffs (i.e. changes between local graph and workspace HEAD)
     */
    workspaceLocalLatestRevisionState;
    /**
     * Store the entities from remote workspace HEAD revision.
     * This can be used for computing the diffs between local workspace and remote workspace to check if the local workspace is out-of-sync
     */
    workspaceRemoteLatestRevisionState;
    aggregatedWorkspaceChanges = []; // review/merge-request changes
    aggregatedProjectLatestChanges = []; // project latest changes - used for updating workspace
    aggregatedWorkspaceRemoteChanges = []; // review/merge-request changes
    potentialWorkspaceUpdateConflicts = []; // potential conflicts when updating workspace (derived from aggregated workspace changes and project latest changes)
    potentialWorkspacePullConflicts = [];
    /**
     * For conflict resolution, the procedure is split into 2 steps:
     * 1. The user resolves conflicts (no graph is built at this point)
     * 2. The user marks all conflicts as resolved and starts building the graph to fix any residual problems
     *
     * Ideally, we would like to use the live diff (conflict resolution BASE <-> local graph), but since for step 1
     * we do not build the graph, this is not possible, so we have to use the following to store the diff until we move to step 2
     */
    aggregatedConflictResolutionChanges = [];
    conflicts = []; // conflicts in conflict resolution mode (derived from aggregated workspace changes and conflict resolution changes)
    resolutions = [];
    currentGraphHash;
    observerContext;
    constructor(editorStore, graphState) {
        makeObservable(this, {
            resolutions: observable,
            currentGraphHash: observable,
            projectLatestRevisionState: observable.ref,
            conflictResolutionBaseRevisionState: observable.ref,
            conflictResolutionHeadRevisionState: observable.ref,
            workspaceBaseRevisionState: observable.ref,
            workspaceLocalLatestRevisionState: observable.ref,
            workspaceRemoteLatestRevisionState: observable,
            aggregatedWorkspaceChanges: observable.ref,
            aggregatedProjectLatestChanges: observable.ref,
            aggregatedWorkspaceRemoteChanges: observable.ref,
            potentialWorkspacePullConflicts: observable.ref,
            potentialWorkspaceUpdateConflicts: observable.ref,
            aggregatedConflictResolutionChanges: observable.ref,
            conflicts: observable.ref,
            setAggregatedProjectLatestChanges: action,
            setPotentialWorkspaceUpdateConflicts: action,
            stop: action,
            start: action,
            computeAggregatedWorkspaceChanges: flow,
            computeAggregatedProjectLatestChanges: flow,
            computeAggregatedConflictResolutionChanges: flow,
            computeWorkspaceUpdateConflicts: flow,
            computeConflictResolutionConflicts: flow,
            computeEntityChangeConflicts: flow,
            computeLocalChanges: flow,
            computeLocalChangesInTextMode: flow,
            computeAggregatedWorkspaceRemoteChanges: flow,
            observeGraph: flow,
        });
        this.editorStore = editorStore;
        this.graphState = graphState;
        this.workspaceLocalLatestRevisionState = new RevisionChangeDetectionState(editorStore, graphState);
        this.workspaceRemoteLatestRevisionState = new RevisionChangeDetectionState(editorStore, graphState);
        this.workspaceBaseRevisionState = new RevisionChangeDetectionState(editorStore, graphState);
        this.projectLatestRevisionState = new RevisionChangeDetectionState(editorStore, graphState);
        // conflict resolution
        this.conflictResolutionHeadRevisionState = new RevisionChangeDetectionState(editorStore, graphState);
        this.conflictResolutionBaseRevisionState = new RevisionChangeDetectionState(editorStore, graphState);
        this.observerContext = new ObserverContext(this.editorStore.pluginManager.getPureGraphManagerPlugins());
    }
    setAggregatedProjectLatestChanges(diffs) {
        this.aggregatedProjectLatestChanges = diffs;
    }
    setAggregatedWorkspaceRemoteChanges(diffs) {
        this.aggregatedWorkspaceRemoteChanges = diffs;
    }
    setPotentialWorkspaceUpdateConflicts(conflicts) {
        this.potentialWorkspaceUpdateConflicts = conflicts;
    }
    setPotentialWorkspacePullConflicts(conflicts) {
        this.potentialWorkspacePullConflicts = conflicts;
    }
    getCurrentGraphHash() {
        return hashObject(Array.from(this.snapshotLocalEntityHashesIndex(true).entries())
            // sort to ensure this array order does not affect change detection status
            .sort((a, b) => a[0].localeCompare(b[0]))
            .map(([key, value]) => `${key}@${value}`));
    }
    stop(force = false) {
        this.changeDetectionReaction?.();
        this.changeDetectionReaction = undefined;
        if (force) {
            this.initState.fail();
        }
        else {
            this.initState.reset();
        }
    }
    /**
     * The change detection check is not free, although due to the power of mobx's computed, this is really fast
     * but we want to use a reaction here instead of having changes as a computed getter is that:
     * 1. We want to debounce the action
     * 2. We want to control when we would start observing for changes (this is useful since we need to compute the initial
     * hashes index before this, otherwise users will get false report about the number of changes)
     * This function might cause the UI to jank the since it involves expensive computation of the all the elements' hashes
     * for the first time. Currently there is no workaround so we might need to comeback and evaluate this
     */
    start() {
        this.changeDetectionReaction?.();
        /**
         * It seems like the reaction action is not always called in tests, causing fluctuation in
         * code coverage report for this file. As such, for test, we would want to disable throttling
         * to avoid timing issue.
         *
         * See https://docs.codecov.io/docs/unexpected-coverage-changes
         * See https://community.codecov.io/t/codecov-reporting-impacted-files-for-unchanged-and-completely-unrelated-file/2635
         */
        // eslint-disable-next-line no-process-env
        const throttleDuration = process.env.NODE_ENV === 'test' ? 0 : 1000;
        /**
         * For the reaction, the data we observe is the snapshot of the current graph's hash index.
         * This will loop through all elements' and compute the hashCode for the first time
         * so in subsequent calls this could be fast. This is expensive and the optimization we choose is
         * to parallelize using promises, but since `mobx` only tracks dependency synchronously,
         * we would lose the benefit of `mobx` computation
         * See https://github.com/danielearwicker/computed-async-mobx#gotchas
         * See https://github.com/mobxjs/mobx/issues/668
         * See https://github.com/mobxjs/mobx/issues/872
         *
         * So now, we have 2 options:
         * 1. We can let the UI freezes up for a short while by calling `this.snapshotCurrentHashesIndex(true)`
         * 2. We will use `keepAlive` for elements that we care about for building the hashes index, i.e. class, mapping, diagram, etc.
         *
         * We will go with (2) but we have to note that `keepAlive` can cause memory leak. As such, activating `keepAlive` is
         * a fairly non-trivial step, see {@link preComputeGraphElementHashes} for more details
         *
         * See https://mobx.js.org/computeds.html#keepalive
         * See https://medium.com/terria/when-and-why-does-mobxs-keepalive-cause-a-memory-leak-8c29feb9ff55
         */
        this.changeDetectionReaction = reaction(() => this.getCurrentGraphHash(), () => {
            flowResult(this.computeLocalChanges(true)).catch(noop());
            this.currentGraphHash = this.getCurrentGraphHash();
        }, {
            // fire reaction immediately to compute the first changeset
            fireImmediately: true,
            // throttle the call
            delay: throttleDuration,
        });
        // set initial current graph hash
        this.currentGraphHash = this.getCurrentGraphHash();
        // dispose and remove the disposers for `keepAlive` computations for elements' hash code
        this.graphElementHashCodeKeepAliveComputationDisposers.forEach((disposer) => disposer());
        this.graphElementHashCodeKeepAliveComputationDisposers = [];
        this.initState.pass();
    }
    snapshotLocalEntityHashesIndex(quiet) {
        const startTime = Date.now();
        const snapshot = new Map();
        this.editorStore.graphManagerState.graph.allOwnElements.forEach((el) => snapshot.set(el.path, el.hashCode));
        if (!quiet) {
            this.editorStore.applicationStore.logService.info(LogEvent.create(LEGEND_STUDIO_APP_EVENT.CHANGE_DETECTION_BUILD_GRAPH_HASHES_INDEX__SUCCESS), Date.now() - startTime, 'ms');
        }
        return snapshot;
    }
    computeAggregatedChangesBetweenStates = async (fromState, toState, quiet) => {
        const startTime = Date.now();
        let changes = [];
        if (!fromState.isBuildingEntityHashesIndex &&
            !toState.isBuildingEntityHashesIndex) {
            const originalPaths = new Set(Array.from(fromState.entityHashesIndex.keys()));
            await Promise.all(Array.from(toState.entityHashesIndex.entries()).map(([elementPath, hashCode]) => promisify(() => {
                const originalElementHashCode = fromState.entityHashesIndex.get(elementPath);
                if (!originalElementHashCode) {
                    changes.push(new EntityDiff(undefined, elementPath, EntityChangeType.CREATE));
                }
                else if (originalElementHashCode !== hashCode) {
                    changes.push(new EntityDiff(elementPath, elementPath, EntityChangeType.MODIFY));
                }
                originalPaths.delete(elementPath);
            })));
            changes = changes.concat(Array.from(originalPaths).map((path) => new EntityDiff(path, undefined, EntityChangeType.DELETE)));
            if (!quiet) {
                this.editorStore.applicationStore.logService.info(LogEvent.create(LEGEND_STUDIO_APP_EVENT.CHANGE_DETECTION_COMPUTE_CHANGES__SUCCESS), Date.now() - startTime, 'ms');
            }
        }
        return changes;
    };
    *computeAggregatedWorkspaceChanges(quiet) {
        this.aggregatedWorkspaceChanges =
            (yield this.computeAggregatedChangesBetweenStates(this.workspaceBaseRevisionState, this.workspaceLocalLatestRevisionState, quiet));
        yield Promise.all([
            this.computeWorkspaceUpdateConflicts(quiet),
            this.computeConflictResolutionConflicts(quiet),
        ]);
    }
    *computeAggregatedWorkspaceRemoteChanges(quiet) {
        this.aggregatedWorkspaceRemoteChanges =
            (yield this.computeAggregatedChangesBetweenStates(this.workspaceLocalLatestRevisionState, this.workspaceRemoteLatestRevisionState, quiet));
        const conflicts = (yield flowResult(this.computeEntityChangeConflicts(this.workspaceLocalLatestRevisionState.changes, this.aggregatedWorkspaceRemoteChanges, this.snapshotLocalEntityHashesIndex(), this.workspaceRemoteLatestRevisionState.entityHashesIndex)));
        this.setPotentialWorkspacePullConflicts(conflicts);
    }
    *computeAggregatedProjectLatestChanges(quiet) {
        this.aggregatedProjectLatestChanges =
            (yield this.computeAggregatedChangesBetweenStates(this.workspaceBaseRevisionState, this.projectLatestRevisionState, quiet));
        yield flowResult(this.computeWorkspaceUpdateConflicts(quiet));
    }
    *computeAggregatedConflictResolutionChanges(quiet) {
        this.aggregatedConflictResolutionChanges =
            (yield this.computeAggregatedChangesBetweenStates(this.conflictResolutionBaseRevisionState, this.conflictResolutionHeadRevisionState, quiet));
    }
    /**
     * Workspace update conflicts are computed between 2 sets of changes:
     * 1. Incoming changes: changes between workspace BASE revision and project LATEST revision
     * 2. Current changes: changes between workspace BASE revision and workspace HEAD revision
     */
    *computeWorkspaceUpdateConflicts(quiet) {
        const startTime = Date.now();
        this.potentialWorkspaceUpdateConflicts = (yield flowResult(this.computeEntityChangeConflicts(this.aggregatedWorkspaceChanges, this.aggregatedProjectLatestChanges, this.workspaceLocalLatestRevisionState.entityHashesIndex, this.projectLatestRevisionState.entityHashesIndex)));
        if (!quiet) {
            this.editorStore.applicationStore.logService.info(LogEvent.create(LEGEND_STUDIO_APP_EVENT.CHANGE_DETECTION_COMPUTE_WORKSPACE_UPDATE_CONFLICTS__SUCCESS), Date.now() - startTime, 'ms');
        }
    }
    /**
     * Conflict resolution conflicts are computed between 2 sets of changes:
     * 1. Incoming changes: changes between workspace BASE revision and conflict resolution BASE revision
     * 2. Current changes: changes between workspace BASE revision and workspace HEAD revision
     */
    *computeConflictResolutionConflicts(quiet) {
        const aggregatedUpdateChanges = (yield this.computeAggregatedChangesBetweenStates(this.workspaceBaseRevisionState, this.conflictResolutionBaseRevisionState, quiet));
        const startTime = Date.now();
        this.conflicts = (yield flowResult(this.computeEntityChangeConflicts(this.aggregatedWorkspaceChanges, aggregatedUpdateChanges, this.workspaceLocalLatestRevisionState.entityHashesIndex, this.conflictResolutionBaseRevisionState.entityHashesIndex)));
        if (!quiet) {
            this.editorStore.applicationStore.logService.info(LogEvent.create(LEGEND_STUDIO_APP_EVENT.CHANGE_DETECTION_COMPUTE_CONFLICT_RESOLUTION_CONFLICTS__SUCCESS), Date.now() - startTime, 'ms');
        }
    }
    /**
     * This function computes the entity change conflicts between 2 set of entity changes (let's call them incoming changes and current changes).
     * For a more comprehensive explanation, we take a look at how we can use this to compute potential conflicts during workspace update:
     *
     * To compute potential conflicts during workspace update, we must base off the project latest changes [incChg] (workspace BASE <-> project HEAD)
     * and the merge request changes [currChng] (workspace BASE <-> workspace HEAD). We have a case table below (`N.A.` means it's impossible cases)
     * For cases we with `conflict` there might be potential conflicts as the change to the entity appear in both [incChg] and [currChng]. But we must
     * note that this is `potential` because we cannot be too sure how SDCL server handle merging these during update.
     *
     * NOTE: it's important to remember that these are truly potential conflicts, because of git merge mechanism,
     * it will apply one intermediate commit at a time, this means that if, we have a file A:
     *
     * Workspace change: 1. A is deleted; 2. A is created with content `a`
     * Project latest change: 1. A is modified with content `a`
     *
     * These could mean no conflict from our computation but is a conflict when Git tries to merge.
     *
     * NOTE: Also, there could be strange case for SVN that a file can be DELETED and CREATED, it's called "replace".
     *
     *             | [incChg] |          |          |          |
     * -----------------------------------------------------------
     *  [currChng] |          |  CREATE  |  DELETE  |  MODIFY  |
     * -----------------------------------------------------------
     *             |  CREATE  | conflict |   N.A.   |   N.A.   |
     * -----------------------------------------------------------
     *             |  DELETE  |   N.A.   |   none   | conflict |
     * -----------------------------------------------------------
     *             |  MODIFY  |   N.A.   | conflict | conflict |
     * -----------------------------------------------------------
     */
    *computeEntityChangeConflicts(currentChanges, incomingChanges, currentChangeEntityHashesIndex, incomingChangeEntityHashesIndex) {
        const conflicts = [];
        const currentChangesMap = currentChanges.reduce((diffMap, currentDiff) => diffMap.set(currentDiff.entityPath, currentDiff), new Map());
        const incomingChangesMap = incomingChanges.reduce((diffMap, currentDiff) => diffMap.set(currentDiff.entityPath, currentDiff), new Map());
        yield Promise.all(Array.from(incomingChangesMap.entries()).map(([entityPath, incomingChange]) => promisify(() => {
            const currentChange = currentChangesMap.get(entityPath); // find the change on the same entity in the set of current changes
            if (currentChange) {
                if ((currentChange.entityChangeType === EntityChangeType.CREATE &&
                    incomingChange.entityChangeType ===
                        EntityChangeType.CREATE) ||
                    (currentChange.entityChangeType === EntityChangeType.MODIFY &&
                        incomingChange.entityChangeType === EntityChangeType.MODIFY)) {
                    // if the two entities are identical, we can guarantee no conflict happens, otherwise, depending on the SDLC server, we might get a conflict.
                    // NOTE: we actually want the potential conflict to be a real conflict in this case because SDLC server while attempting to merge the protocol JSON
                    // might actually mess up the entity, which is very bad
                    if (currentChangeEntityHashesIndex.get(entityPath) !==
                        incomingChangeEntityHashesIndex.get(entityPath)) {
                        conflicts.push(new EntityChangeConflict(entityPath, incomingChange, currentChange));
                    }
                }
                else if ((currentChange.entityChangeType === EntityChangeType.DELETE &&
                    incomingChange.entityChangeType ===
                        EntityChangeType.MODIFY) ||
                    (currentChange.entityChangeType === EntityChangeType.MODIFY &&
                        incomingChange.entityChangeType === EntityChangeType.DELETE)) {
                    conflicts.push(new EntityChangeConflict(entityPath, incomingChange, currentChange));
                }
                else if (currentChange.entityChangeType === EntityChangeType.DELETE &&
                    incomingChange.entityChangeType === EntityChangeType.DELETE) {
                    // do nothing
                }
                else {
                    throw new IllegalStateError(`Detected unfeasible state while computing entity change conflict for entity '${entityPath}', with current change: ${shallowStringify(currentChange)}, and incoming change: ${shallowStringify(incomingChange)}`);
                }
            }
        })));
        return conflicts;
    }
    /**
     * NOTE: here we have not dealt with non-entity changes like project dependency for example.
     * We will have to count that as part of the change in the future.
     */
    *computeLocalChanges(quiet) {
        const startTime = Date.now();
        yield Promise.all([
            this.workspaceLocalLatestRevisionState.computeChanges(quiet), // for local changes detection
            this.conflictResolutionBaseRevisionState.computeChanges(quiet), // for conflict resolution changes detection
        ]);
        if (!quiet) {
            this.editorStore.applicationStore.logService.info(LogEvent.create(LEGEND_STUDIO_APP_EVENT.CHANGE_DETECTION_COMPUTE_CHANGES__SUCCESS), Date.now() - startTime, 'ms');
        }
    }
    *computeLocalChangesInTextMode(currentEntities, quiet) {
        const startTime = Date.now();
        yield Promise.all([
            this.workspaceLocalLatestRevisionState.computeChangesInTextMode(currentEntities, quiet),
        ]);
        if (!quiet) {
            this.editorStore.applicationStore.logService.info(LogEvent.create(LEGEND_STUDIO_APP_EVENT.CHANGE_DETECTION_COMPUTE_CHANGES__SUCCESS), Date.now() - startTime, 'ms');
        }
        this.initState.pass();
    }
    *observeGraph() {
        if (this.initState.hasSucceeded) {
            throw new IllegalStateError(`Can't observe graph: change detection must be stopped first`);
        }
        if (this.editorStore.graphManagerState.graph.origin) {
            throw new IllegalStateError(`Can't observe graph: can't change graph with sdlc pointer`);
        }
        this.graphObserveState.inProgress();
        this.graphObserveState.setMessage(`Observing graph...`);
        const startTime = Date.now();
        // NOTE: this method has to be done synchronously in `action` context
        // to make sure `mobx` react to observables from the graph, such as its element indices
        observe_Graph(this.editorStore.graphManagerState.graph);
        // this will be done asynchronously to improve performance
        yield observe_GraphElements(this.editorStore.graphManagerState.graph, this.editorStore.changeDetectionState.observerContext);
        this.editorStore.applicationStore.logService.info(LogEvent.create(LEGEND_STUDIO_APP_EVENT.CHANGE_DETECTION_OBSERVE_GRAPH__SUCCESS), '[ASYNC]', Date.now() - startTime, 'ms');
        this.graphObserveState.setMessage(undefined);
        this.graphObserveState.pass();
    }
    /**
     * Call `get hashCode()` on each element once so we trigger the first time we compute the hash for that element.
     * Notice that we do this in asynchronous manner to not block the main execution thread, as this is quiet an expensive
     * task.
     *
     * We also want to take advantage of `mobx computed` here so we save time when starting change detection. However,
     * since `mobx computed` does not track async contexts, we have to use the `keepAlive` option for `computed`
     *
     * To avoid memory leak potentially caused by `keepAlive`, we use `keepAlive` utility from `mobx-utils`
     * so we could manually dispose `keepAlive` later after we already done with starting change detection.
     */
    async preComputeGraphElementHashes() {
        const startTime = Date.now();
        const disposers = [];
        if (this.editorStore.graphManagerState.graph.allOwnElements.length) {
            await Promise.all(this.editorStore.graphManagerState.graph.allOwnElements.map((element) => promisify(() => {
                if (isComputedProp(element, 'hashCode')) {
                    disposers.push(keepAlive(element, 'hashCode'));
                }
                // manually trigger hash code computation
                // eslint-disable-next-line @typescript-eslint/no-unused-expressions
                element.hashCode;
            })));
        }
        // save the `keepAlive` computation disposers to dispose after we start change detection
        // so the first call of change detection still get the benefit of computed value
        this.graphElementHashCodeKeepAliveComputationDisposers = disposers;
        this.editorStore.applicationStore.logService.info(LogEvent.create(LEGEND_STUDIO_APP_EVENT.CHANGE_DETECTION_PRECOMPUTE_GRAPH_HASHES__SUCCESS), '[ASYNC]', Date.now() - startTime, 'ms');
    }
}
//# sourceMappingURL=ChangeDetectionState.js.map