/**
 * 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 { observable, flow, action, computed, makeObservable, flowResult, } from 'mobx';
import { LogEvent, assertTrue, assertErrorThrown, guaranteeNonNullable, isNonNullable, ActionState, } from '@finos/legend-shared';
import { ElementEditorState } from './element-editor-state/ElementEditorState.js';
import { ElementFileGenerationState } from './element-editor-state/ElementFileGenerationState.js';
import { ArtifactGenerationExtensionResult, Class, Enumeration, GenerationSpecification, ELEMENT_PATH_DELIMITER, } from '@finos/legend-graph';
import { ExternalFormatState } from './ExternalFormatState.js';
import { ExplorerTreeRootPackageLabel } from '../ExplorerTreeState.js';
import { LEGEND_STUDIO_APP_EVENT } from '../../../__lib__/LegendStudioEvent.js';
import { FileSystem_Directory, FileSystem_File, GENERATION_FILE_ROOT_NAME, buildFileSystemDirectory, getFileSystemTreeData, reprocessOpenNodes, openNode, populateDirectoryTreeNodeChildren, } from '../utils/FileSystemTreeUtils.js';
import { ArtifactGenerationViewerState } from './ArtifactGenerationViewerState.js';
import { generationSpecification_addFileGeneration, generationSpecification_addGenerationElement, } from '../../graph-modifier/DSL_Generation_GraphModifierHelper.js';
export const DEFAULT_GENERATION_SPECIFICATION_NAME = 'MyGenerationSpecification';
export class DEPREACTED_GlobalFileGenerationState {
    graphGenerationState;
    editorStore;
    // NOTE: this will eventually be removed once we also do model/schema import using external format
    // See https://github.com/finos/legend-studio/issues/866
    fileGenerationConfigurations = [];
    constructor(graphGenerationState, editorStore) {
        makeObservable(this, {
            fileGenerationConfigurations: observable,
            fileGenerationConfigurationOptions: computed,
            supportedFileGenerationConfigurationsForCurrentElement: computed,
            setFileGenerationConfigurations: action,
            fetchAvailableFileGenerationDescriptions: flow,
            DEPREACTED_generateFiles: flow,
        });
        this.graphGenerationState = graphGenerationState;
        this.editorStore = editorStore;
    }
    get fileGenerationConfigurationOptions() {
        return this.fileGenerationConfigurations
            .toSorted((a, b) => a.label.localeCompare(b.label))
            .map((config) => ({
            label: config.label,
            value: config.key,
        }));
    }
    get supportedFileGenerationConfigurationsForCurrentElement() {
        if (this.editorStore.tabManagerState.currentTab instanceof ElementEditorState) {
            const currentElement = this.editorStore.tabManagerState.currentTab.element;
            // NOTE: For now we only allow classes and enumerations for all types of generations.
            const extraFileGenerationScopeFilterConfigurations = this.editorStore.pluginManager
                .getApplicationPlugins()
                .flatMap((plugin) => plugin.getExtraFileGenerationScopeFilterConfigurations?.() ?? []);
            return this.fileGenerationConfigurations.filter((generationType) => {
                const scopeFilters = extraFileGenerationScopeFilterConfigurations.filter((configuration) => configuration.type.toLowerCase() === generationType.key);
                if (scopeFilters.length) {
                    return scopeFilters.some((scopeFilter) => scopeFilter.filter(currentElement));
                }
                return (currentElement instanceof Class ||
                    currentElement instanceof Enumeration);
            });
        }
        return [];
    }
    /**
     * Generated file generations in the graph.
     * NOTE: This method does not update graph and application only the files are generated.
     */
    *DEPREACTED_generateFiles(generationOutputIndex) {
        try {
            const generationSpecs = this.editorStore.graphManagerState.graph.ownGenerationSpecifications;
            if (!generationSpecs.length) {
                return;
            }
            assertTrue(generationSpecs.length === 1, `Can't generate models: only one generation specification permitted to generate`);
            const generationSpec = generationSpecs[0];
            const fileGenerations = generationSpec.fileGenerations;
            // we don't need to keep 'fetching' the main model as it won't grow with each file generation
            for (const fileGeneration of fileGenerations) {
                let result = [];
                try {
                    const mode = this.editorStore.graphState.graphGenerationState.globalFileGenerationState.getFileGenerationConfiguration(fileGeneration.value.type).generationMode;
                    result =
                        (yield this.editorStore.graphManagerState.graphManager.generateFile(fileGeneration.value, mode, this.editorStore.graphManagerState.graph, this.editorStore.graphEditorMode.getGraphTextInputOption()));
                }
                catch (error) {
                    assertErrorThrown(error);
                    throw new Error(`Can't generate files using specification '${fileGeneration.value.path}'. Error: ${error.message}`);
                }
                generationOutputIndex.set(fileGeneration.value.path, result);
            }
        }
        catch (error) {
            assertErrorThrown(error);
            this.editorStore.applicationStore.logService.error(LogEvent.create(LEGEND_STUDIO_APP_EVENT.GENERATION_FAILURE), error);
            this.editorStore.graphState.editorStore.applicationStore.notificationService.notifyError(`${error.message}`);
        }
    }
    setFileGenerationConfigurations(fileGenerationConfigurations) {
        this.fileGenerationConfigurations = fileGenerationConfigurations;
    }
    getFileGenerationConfiguration(type) {
        return guaranteeNonNullable(this.fileGenerationConfigurations.find((config) => config.key === type), `Can't find configuration description for file generation type '${type}'`);
    }
    *fetchAvailableFileGenerationDescriptions() {
        try {
            const availableFileGenerationDescriptions = (yield this.editorStore.graphManagerState.graphManager.getAvailableGenerationConfigurationDescriptions());
            this.setFileGenerationConfigurations(availableFileGenerationDescriptions);
            this.editorStore.elementGenerationStates =
                this.fileGenerationConfigurations.map((config) => new ElementFileGenerationState(this.editorStore, config.key));
        }
        catch (error) {
            assertErrorThrown(error);
            this.editorStore.applicationStore.logService.error(LogEvent.create(LEGEND_STUDIO_APP_EVENT.GENERATION_FAILURE), error);
            this.editorStore.applicationStore.notificationService.notifyError(error);
        }
    }
}
export class GraphGenerationState {
    editorStore;
    isRunningGlobalGenerate = false;
    generatedEntities = new Map();
    clearingGenerationEntitiesState = ActionState.create();
    externalFormatState;
    globalFileGenerationState;
    // file generation output
    rootFileDirectory;
    filesIndex = new Map();
    selectedNode;
    // Note for now generation of artifact generations is set to false.
    // This is to explore the performance impact of inlcuding this in the generation action and whether we always
    // generate artifacts or add inputs to the api to choose which ones we generate.
    enableArtifactGeneration = false;
    constructor(editorStore) {
        makeObservable(this, {
            isRunningGlobalGenerate: observable,
            generatedEntities: observable.shallow,
            clearingGenerationEntitiesState: observable,
            externalFormatState: observable,
            enableArtifactGeneration: observable,
            globalFileGenerationState: observable,
            rootFileDirectory: observable,
            filesIndex: observable,
            selectedNode: observable.ref,
            processGenerationResult: action,
            reprocessGenerationFileState: action,
            setEnableArtifactGeneration: action,
            reprocessNodeTree: action,
            onTreeNodeSelect: action,
            setSelectedNode: action,
            emptyGeneratedArtifacts: action,
            possiblyAddMissingGenerationSpecifications: flow,
            globalGenerate: flow,
            generateModels: flow,
            generateArtifacts: flow,
            clearGenerations: flow,
        });
        this.editorStore = editorStore;
        this.externalFormatState = new ExternalFormatState(editorStore);
        this.globalFileGenerationState = new DEPREACTED_GlobalFileGenerationState(this, editorStore);
        this.rootFileDirectory = new FileSystem_Directory(GENERATION_FILE_ROOT_NAME);
    }
    setEnableArtifactGeneration(val) {
        this.enableArtifactGeneration = val;
    }
    findGenerationParentPath(genChildPath) {
        const genEntity = Array.from(this.generatedEntities.entries()).find(([, genEntities]) => genEntities.find((m) => m.path === genChildPath));
        return genEntity?.[0];
    }
    /**
     * Global generation is tied to the generation specification of the project. Every time a generation element
     * is added, they will be added to the generation specification
     */
    *globalGenerate() {
        if (this.editorStore.graphState.checkIfApplicationUpdateOperationIsRunning()) {
            return;
        }
        this.isRunningGlobalGenerate = true;
        try {
            yield flowResult(this.generateModels());
            yield flowResult(this.generateArtifacts());
        }
        catch (error) {
            assertErrorThrown(error);
            this.editorStore.applicationStore.logService.error(LogEvent.create(LEGEND_STUDIO_APP_EVENT.GENERATION_FAILURE), error);
            this.editorStore.graphState.editorStore.applicationStore.notificationService.notifyError(`${error.message}`);
        }
        finally {
            this.isRunningGlobalGenerate = false;
        }
    }
    *generateModels() {
        try {
            this.generatedEntities = new Map(); // reset the map of generated entities
            const generationSpecs = this.editorStore.graphManagerState.graph.ownGenerationSpecifications;
            if (!generationSpecs.length) {
                return;
            }
            assertTrue(generationSpecs.length === 1, `Can't generate models: only one generation specification permitted to generate`);
            const generationSpec = generationSpecs[0];
            const generationNodes = generationSpec.generationNodes;
            for (let i = 0; i < generationNodes.length; i++) {
                const node = generationNodes[i];
                let generatedEntities = [];
                try {
                    generatedEntities =
                        (yield this.editorStore.graphManagerState.graphManager.generateModel(node.generationElement.value, this.editorStore.graphManagerState.graph, this.editorStore.graphEditorMode.getGraphTextInputOption()));
                }
                catch (error) {
                    assertErrorThrown(error);
                    throw new Error(`Can't generate models: failure occured at step ${i + 1} with specification '${node.generationElement.value.path}'. Error: ${error.message}`);
                }
                this.generatedEntities.set(node.generationElement.value.path, generatedEntities);
                yield flowResult(this.editorStore.graphState.updateGenerationGraphAndApplication());
            }
        }
        catch (error) {
            assertErrorThrown(error);
            this.editorStore.applicationStore.logService.error(LogEvent.create(LEGEND_STUDIO_APP_EVENT.GENERATION_FAILURE), error);
            this.editorStore.graphState.editorStore.applicationStore.notificationService.notifyError(`${error.message}`);
        }
    }
    /**
     * Generated artifacts generations in graph
     * NOTE: This method does not update graph and application only the files are generated.
     */
    *generateArtifacts() {
        try {
            this.emptyGeneratedArtifacts();
            const generationOutputIndex = new Map();
            // handle deprecated file generations
            yield flowResult(this.globalFileGenerationState.DEPREACTED_generateFiles(generationOutputIndex));
            let artifacts = new ArtifactGenerationExtensionResult();
            if (this.enableArtifactGeneration) {
                artifacts =
                    (yield this.editorStore.graphManagerState.graphManager.generateArtifacts(this.editorStore.graphManagerState.graph, this.editorStore.graphEditorMode.getGraphTextInputOption()));
            }
            // handle results
            this.processGenerationResult(artifacts, generationOutputIndex);
        }
        catch (error) {
            assertErrorThrown(error);
            this.editorStore.applicationStore.logService.error(LogEvent.create(LEGEND_STUDIO_APP_EVENT.GENERATION_FAILURE), error);
            this.editorStore.graphState.editorStore.applicationStore.notificationService.notifyError(`${error.message}`);
        }
    }
    // Artifact Generation Tree
    processGenerationResult(artifacts, generationOutputIndex) {
        // empty file index and the directory, we keep the open nodes to reprocess them
        this.emptyGeneratedArtifacts();
        const directoryTreeData = this.editorStore.graphState.editorStore.explorerTreeState
            .artifactsGenerationTreeData;
        const openedNodeIds = directoryTreeData
            ? Array.from(directoryTreeData.nodes.values())
                .filter((node) => node.isOpen)
                .map((node) => node.id)
            : [];
        // we read the generation outputs and clean
        const generationResultIndex = new Map();
        // handle DEPRECATED file gen for backward compatible
        Array.from(generationOutputIndex.entries()).forEach((entry) => {
            const generator = this.editorStore.graphManagerState.graph.getNullableFileGeneration(entry[0]);
            const rootFolder = generator?.generationOutputPath ??
                generator?.path.split(ELEMENT_PATH_DELIMITER).join('_');
            const generationOutputs = entry[1];
            generationOutputs.forEach((genOutput) => {
                genOutput.cleanFileName(rootFolder);
                if (generationResultIndex.has(genOutput.fileName)) {
                    this.editorStore.applicationStore.logService.warn(LogEvent.create(LEGEND_STUDIO_APP_EVENT.GENERATION_FAILURE), `Found 2 generation outputs with same path '${genOutput.fileName}'`);
                }
                generationResultIndex.set(genOutput.fileName, {
                    value: genOutput,
                    parentId: generator?.path,
                });
            });
        });
        artifacts.values
            .map((v) => v.artifactsByExtensionElements)
            .flat()
            .forEach((artifactByElement) => {
            artifactByElement.files.forEach((genOutput) => {
                genOutput.cleanFileName();
                if (generationResultIndex.has(genOutput.fileName)) {
                    this.editorStore.applicationStore.logService.warn(LogEvent.create(LEGEND_STUDIO_APP_EVENT.GENERATION_FAILURE), `Found 2 generation outputs with same path '${genOutput.fileName}'`);
                }
                generationResultIndex.set(genOutput.fileName, {
                    value: genOutput,
                    parentId: artifactByElement.element,
                });
            });
        });
        // take generation outputs and put them into the root directory
        buildFileSystemDirectory(this.rootFileDirectory, generationResultIndex, this.filesIndex);
        // after building root directory set the generation tree data
        this.editorStore.graphState.editorStore.explorerTreeState.setArtifactsGenerationTreeData(getFileSystemTreeData(this.editorStore.graphState.graphGenerationState.rootFileDirectory, ExplorerTreeRootPackageLabel.FILE_GENERATION));
        this.editorStore.graphState.editorStore.explorerTreeState.setArtifactsGenerationTreeData(this.reprocessNodeTree(Array.from(generationResultIndex.values()), this.editorStore.graphState.editorStore.explorerTreeState.getArtifactsGenerationTreeData(), openedNodeIds));
        this.editorStore.tabManagerState.tabs =
            this.editorStore.tabManagerState.tabs
                .map((e) => this.reprocessGenerationFileState(e))
                .filter(isNonNullable);
    }
    reprocessGenerationFileState(editorState) {
        if (editorState instanceof ArtifactGenerationViewerState) {
            const fileNode = this.filesIndex.get(editorState.artifact.path);
            if (fileNode) {
                editorState.artifact = fileNode;
                return editorState;
            }
            else {
                return undefined;
            }
        }
        return editorState;
    }
    reprocessNodeTree(generationResult, treeData, openedNodeIds) {
        reprocessOpenNodes(treeData, this.filesIndex, this.rootFileDirectory, openedNodeIds, true);
        const selectedFileNodePath = this.selectedNode?.fileNode.path ??
            (generationResult.length === 1
                ? generationResult[0].value.fileName
                : undefined);
        if (selectedFileNodePath) {
            const file = this.filesIndex.get(selectedFileNodePath);
            if (file) {
                const node = openNode(file, treeData, true);
                if (node) {
                    this.onTreeNodeSelect(node, treeData, true);
                }
            }
            else {
                this.selectedNode = undefined;
            }
        }
        return treeData;
    }
    onTreeNodeSelect(node, treeData, reprocess) {
        if (node.childrenIds?.length) {
            node.isOpen = !node.isOpen;
            if (node.fileNode instanceof FileSystem_Directory) {
                populateDirectoryTreeNodeChildren(node, treeData);
            }
        }
        if (!reprocess && node.fileNode instanceof FileSystem_File) {
            this.editorStore.graphEditorMode.openFileSystem_File(node.fileNode);
        }
        this.setSelectedNode(node);
        this.editorStore.graphState.editorStore.explorerTreeState.setArtifactsGenerationTreeData({ ...treeData });
    }
    setSelectedNode(node) {
        if (this.selectedNode) {
            this.selectedNode.isSelected = false;
        }
        if (node) {
            node.isSelected = true;
        }
        this.selectedNode = node;
    }
    emptyGeneratedArtifacts() {
        this.filesIndex = new Map();
        this.rootFileDirectory = new FileSystem_Directory(GENERATION_FILE_ROOT_NAME);
    }
    /**
     * Used to clear generation entities as well as the generation model
     */
    *clearGenerations() {
        this.clearingGenerationEntitiesState.inProgress();
        this.generatedEntities = new Map();
        this.emptyGeneratedArtifacts();
        yield flowResult(this.editorStore.graphState.updateGenerationGraphAndApplication());
        this.clearingGenerationEntitiesState.complete();
    }
    /**
     * Method adds generation specification if
     * 1. no generation specification has been defined in graph
     * 2. there exists a generation element
     */
    *possiblyAddMissingGenerationSpecifications() {
        if (!this.editorStore.graphManagerState.graph.ownGenerationSpecifications
            .length) {
            const modelGenerationElements = this.editorStore.pluginManager
                .getPureGraphManagerPlugins()
                .flatMap((plugin) => plugin.getExtraModelGenerationElementGetters?.() ?? [])
                .flatMap((getter) => getter(this.editorStore.graphManagerState.graph));
            const fileGenerations = this.editorStore.graphManagerState.graph.ownFileGenerations;
            if (modelGenerationElements.length || fileGenerations.length) {
                const generationSpec = new GenerationSpecification(DEFAULT_GENERATION_SPECIFICATION_NAME);
                modelGenerationElements.forEach((e) => generationSpecification_addGenerationElement(generationSpec, e));
                fileGenerations.forEach((e) => generationSpecification_addFileGeneration(generationSpec, e));
                // NOTE: add generation specification at the same package as the first generation element found.
                // we might want to revisit this decision?
                const specPackage = guaranteeNonNullable([...modelGenerationElements, ...fileGenerations][0]?.package);
                yield flowResult(this.editorStore.graphEditorMode.addElement(generationSpec, specPackage.path, false));
            }
        }
    }
}
//# sourceMappingURL=GraphGenerationState.js.map