/**
 * 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 { PRIMITIVE_TYPE, ELEMENT_PATH_DELIMITER, ROOT_PACKAGE_NAME, } from '../../../../../../../graph/MetaModelConst.js';
import { uniq, guaranteeNonNullable, assertNonEmptyString, guaranteeType, returnUndefOnError, AssertionError, } from '@finos/legend-shared';
import { GenericType } from '../../../../../../../graph/metamodel/pure/packageableElements/domain/GenericType.js';
import { ImportAwareCodeSection, } from '../../../../../../../graph/metamodel/pure/packageableElements/section/Section.js';
import { StereotypeImplicitReference } from '../../../../../../../graph/metamodel/pure/packageableElements/domain/StereotypeReference.js';
import { GenericTypeImplicitReference, } from '../../../../../../../graph/metamodel/pure/packageableElements/domain/GenericTypeReference.js';
import { PackageableElementImplicitReference } from '../../../../../../../graph/metamodel/pure/packageableElements/PackageableElementReference.js';
import { TagImplicitReference } from '../../../../../../../graph/metamodel/pure/packageableElements/domain/TagReference.js';
import { PropertyImplicitReference } from '../../../../../../../graph/metamodel/pure/packageableElements/domain/PropertyReference.js';
import { JoinImplicitReference } from '../../../../../../../graph/metamodel/pure/packageableElements/store/relational/model/JoinReference.js';
import { FilterImplicitReference } from '../../../../../../../graph/metamodel/pure/packageableElements/store/relational/model/FilterReference.js';
import { RootFlatDataRecordTypeImplicitReference } from '../../../../../../../graph/metamodel/pure/packageableElements/store/flatData/model/RootFlatDataRecordTypeReference.js';
import { createImplicitRelationReference } from '../../../../../../../graph/metamodel/pure/packageableElements/store/relational/model/RelationReference.js';
import { EnumValueImplicitReference } from '../../../../../../../graph/metamodel/pure/packageableElements/domain/EnumValueReference.js';
import { V1_getRelation } from './helpers/V1_DatabaseBuilderHelper.js';
import { DataType } from '../../../../../../../graph/metamodel/pure/packageableElements/domain/DataType.js';
import { GraphBuilderError } from '../../../../../../../graph-manager/GraphManagerUtils.js';
import { getClassProperty, getEnumValue, getOwnProperty, getStereotype, getTag, newGenericType, } from '../../../../../../../graph/helpers/DomainHelper.js';
import { getFilter, getJoin, } from '../../../../../../../graph/helpers/STO_Relational_Helper.js';
import { getRootRecordType, getSection, } from '../../../../../../../graph/helpers/STO_FlatData_Helper.js';
import { V1_getGenericTypeFullPath } from '../../../helpers/V1_DomainHelper.js';
import { V1_RelationType } from '../../../model/packageableElements/type/V1_RelationType.js';
import { RelationColumn, RelationType, } from '../../../../../../../graph/metamodel/pure/packageableElements/relation/RelationType.js';
import { V1_buildValueSpecification } from './helpers/V1_ValueSpecificationBuilderHelper.js';
import { V1_RawRelationType, } from '../../../model/rawValueSpecification/V1_RawVariable.js';
import { Multiplicity } from '../../../../../../../graph/metamodel/pure/packageableElements/domain/Multiplicity.js';
export const V1_buildFullPath = (packagePath, name) => `${guaranteeNonNullable(packagePath, 'Package path is required')}${ELEMENT_PATH_DELIMITER}${guaranteeNonNullable(name, 'Name is required')}`;
export class V1_GraphBuilderContext {
    autoImports;
    sectionImports = [];
    logService;
    currentSubGraph;
    extensions;
    graph;
    section;
    options;
    constructor(builder) {
        this.logService = builder.logService;
        this.graph = builder.graph;
        this.autoImports = this.graph.autoImports;
        this.currentSubGraph = builder.currentSubGraph;
        this.extensions = builder.extensions;
        this.sectionImports = builder.sectionImports;
        this.section = builder.section;
        this.options = builder.options;
    }
    /**
     * Since we haven't fully supported section index, shortened paths
     * using imports in the graph might need to be fully resolved.
     *
     * To handle this need, we make use of references. References can auto
     * resolve full paths when the section index is deleted. But, when
     * building the graph, we leave the value specifications
     * raw/unprocessed. Hence, we cannot make use of references to do full
     * path resolution, as such, we make a best effort traversal in the model
     * of raw value specifications to resolve path automatically.
     *
     * We create this flag to control the behavior of lambda auto path-resolution.
     * This rewriting behavior should not be done for immutable graphs, such as
     * system, depdendencies, and generation. However, in overall, it would be controlled
     * also by the `TEMPORARY__preserveSectionIndex` flag.
     *
     * NOTE: When we fully support section index, we would certainly need to
     * revise the usefullness of this flag, perhaps, we don't auto-resolve anymore,
     * but this mechanism would still be beneficial as we can keep it as an utility
     * to resolve raw lambdas' paths when the user deliberately delete the section
     * index, for example.
     *
     * https://github.com/finos/legend-studio/issues/1067
     */
    get enableRawLambdaAutoPathResolution() {
        return (this.graph.root.name === ROOT_PACKAGE_NAME.MAIN &&
            !this.options?.TEMPORARY__preserveSectionIndex);
    }
    resolve(path, resolverFn) {
        // Try the find from special types (not user-defined top level types)
        const SPECIAL_TYPES = Object.values(PRIMITIVE_TYPE).concat([]);
        if (SPECIAL_TYPES.includes(path)) {
            return {
                element: resolverFn(path),
            };
        }
        // if the path is a path with package, no resolution from section imports is needed
        if (path.includes(ELEMENT_PATH_DELIMITER)) {
            return {
                element: resolverFn(path),
                isFullPath: true,
            };
        }
        // NOTE: here we make the assumption that we have populated the indices properly so the same element
        // is not referred using 2 different paths in the same element index
        const results = new Map();
        this.autoImports.forEach((importPackage) => {
            try {
                const fullPath = importPackage.path + ELEMENT_PATH_DELIMITER + path;
                const element = resolverFn(fullPath);
                if (element) {
                    results.set(fullPath, {
                        element,
                        resolvedUsingSectionImports: false,
                    });
                }
            }
            catch {
                // do nothing
            }
        });
        // only resolve section imports if there is a section
        if (this.section) {
            this.sectionImports.forEach((importPackage) => {
                try {
                    const fullPath = importPackage.path + ELEMENT_PATH_DELIMITER + path;
                    const element = resolverFn(fullPath);
                    if (element) {
                        results.set(fullPath, {
                            element,
                            resolvedUsingSectionImports: true,
                        });
                    }
                }
                catch {
                    // do nothing
                }
            });
        }
        switch (results.size) {
            /**
             * NOTE: if nothing is found then we will try to find user-defined elements at root package (i.e. no package)
             * We place this after import resolution since we want to emphasize that this type of element has the lowest precedence
             * In fact, due to the restriction that Alloy imposes on element path, the only kinds of element
             * we could find at this level are packages, but they will not fit the type we look for
             * in PURE, since we resolve to CoreInstance, further validation needs to be done to make the resolution complete
             * here we count on the `resolver` to do the validation of the type of element instead
             */
            case 0:
                return {
                    element: resolverFn(path),
                    isFullPath: true,
                };
            case 1:
                return guaranteeNonNullable(Array.from(results.values())[0]);
            default:
                throw new GraphBuilderError(undefined, `Can't resolve element with path '${path}' - multiple matches found [${Array.from(results.keys()).join(', ')}]`);
        }
    }
    /**
     * This method and this class in general demonstrates the difference
     * between explicit and implicit reference.
     * See {@link PackageableElementImplicitReference} for more details.
     *
     * Notice that every method in the resolver ends up creating an implicit reference.
     * It does not matter whether the full path is specified or not (i.e. so almost
     * no inference was done), the resulting reference must be implicit, as we took the
     * input into account when creating this reference.
     */
    createImplicitPackageableElementReference = (path, resolverFn) => {
        const { element, resolvedUsingSectionImports, isFullPath } = this.resolve(path, resolverFn);
        if (!resolvedUsingSectionImports && !isFullPath) {
            return PackageableElementImplicitReference.create(element, path);
        }
        return PackageableElementImplicitReference.resolveFromSection(element, path, resolvedUsingSectionImports ? this.section : undefined);
    };
    resolveStereotype = (stereotypePtr) => {
        assertNonEmptyString(stereotypePtr.profile, `Steoreotype pointer 'profile' field is missing or empty`);
        assertNonEmptyString(stereotypePtr.value, `Steoreotype pointer 'value' field is missing or empty`);
        const ownerReference = this.resolveProfile(stereotypePtr.profile);
        const value = getStereotype(ownerReference.value, stereotypePtr.value);
        return StereotypeImplicitReference.create(ownerReference, value);
    };
    resolveTag = (tagPtr) => {
        assertNonEmptyString(tagPtr.profile, `Tag pointer 'profile' field is missing or empty`);
        assertNonEmptyString(tagPtr.value, `Tag pointer 'value' field is missing or empty`);
        const ownerReference = this.resolveProfile(tagPtr.profile);
        const value = getTag(ownerReference.value, tagPtr.value);
        return TagImplicitReference.create(ownerReference, value);
    };
    resolveGenericType = (path) => {
        const ownerReference = this.resolveType(path);
        const value = new GenericType(ownerReference.value);
        return GenericTypeImplicitReference.create(ownerReference, value);
    };
    resolveGenericTypeFromProtocol = (genericType) => {
        const ownerReference = this.resolveType(V1_getGenericTypeFullPath(genericType));
        const typeArguments = genericType.typeArguments.map((g) => this.resolveGenericTypeFromProtocol(g));
        const value = newGenericType(ownerReference.value, typeArguments);
        return GenericTypeImplicitReference.create(ownerReference, value);
    };
    resolveGenericTypeFromProtocolWithRelationType = (genericType) => {
        let ownerReference;
        const rawType = genericType.rawType;
        if (rawType instanceof V1_RelationType) {
            const _relType = new RelationType(RelationType.ID);
            _relType.columns = rawType.columns.map((_col) => {
                const relCol = new RelationColumn(_col.name, this.resolveGenericTypeFromProtocolWithRelationType(_col.genericType));
                relCol.multiplicity = this.graph.getMultiplicity(_col.multiplicity.lowerBound, _col.multiplicity.upperBound);
                return relCol;
            });
            ownerReference = PackageableElementImplicitReference.create(_relType, V1_RelationType.NAME);
        }
        else {
            ownerReference = this.resolveType(V1_getGenericTypeFullPath(genericType));
        }
        const typeArguments = genericType.typeArguments.map((g) => this.resolveGenericTypeFromProtocolWithRelationType(g));
        const value = newGenericType(ownerReference.value, typeArguments);
        value.typeVariableValues = genericType.typeVariableValues.map((v) => V1_buildValueSpecification(v, this));
        return GenericTypeImplicitReference.create(ownerReference, value);
    };
    resolveRawGenericTypeFromProtocol = (genericType) => {
        let ownerReference;
        const rawType = genericType.rawType;
        if (rawType instanceof V1_RawRelationType) {
            const _relType = new RelationType(RelationType.ID);
            _relType.columns = rawType.columns.map((_col) => {
                const relCol = new RelationColumn(_col.name, this.resolveRawGenericTypeFromProtocol(_col.genericType));
                if (_col.multiplicity) {
                    relCol.multiplicity = this.graph.getMultiplicity(_col.multiplicity.lowerBound, _col.multiplicity.upperBound);
                }
                else {
                    relCol.multiplicity = Multiplicity.ZERO_ONE;
                }
                return relCol;
            });
            ownerReference = PackageableElementImplicitReference.create(_relType, V1_RelationType.NAME);
        }
        else {
            ownerReference = this.resolveType(rawType.fullPath);
        }
        const typeArguments = genericType.typeArguments.map((g) => this.resolveRawGenericTypeFromProtocol(g));
        const value = newGenericType(ownerReference.value, typeArguments);
        value.typeVariableValues = genericType.typeVariableValues.map((v) => V1_buildValueSpecification(v, this));
        return GenericTypeImplicitReference.create(ownerReference, value);
    };
    resolveOwnProperty = (pointer) => {
        assertNonEmptyString(pointer.class, `Property pointer 'class' field is missing or empty`);
        assertNonEmptyString(pointer.property, `Property pointer 'property' field is missing or empty`);
        const ownerReference = this.resolvePropertyOwner(pointer.class);
        const value = getOwnProperty(ownerReference.value, pointer.property);
        return PropertyImplicitReference.create(ownerReference, value);
    };
    resolveProperty = (pointer) => {
        assertNonEmptyString(pointer.class, `Property pointer 'class' field is missing or empty`);
        assertNonEmptyString(pointer.property, `Property pointer 'property' field is missing or empty`);
        const ownerReference = this.resolveClass(pointer.class);
        const value = getClassProperty(ownerReference.value, pointer.property);
        return PropertyImplicitReference.create(ownerReference, value);
    };
    resolveRootFlatDataRecordType = (classMapping) => {
        assertNonEmptyString(classMapping.flatData, `Flat-data class mapping 'flatData' field is missing or empty`);
        assertNonEmptyString(classMapping.sectionName, `Flat-data class mapping 'sectionName' field is missing or empty`);
        const ownerReference = this.resolveFlatDataStore(classMapping.flatData);
        const value = getRootRecordType(getSection(ownerReference.value, classMapping.sectionName));
        return RootFlatDataRecordTypeImplicitReference.create(ownerReference, value);
    };
    resolveRelation = (tablePtr, contextDatabse, allowImplicitToGeneratedDatabse) => {
        assertNonEmptyString(tablePtr.database, `Table pointer 'database' field is missing or empty`);
        assertNonEmptyString(tablePtr.schema, `Table pointer 'schema' field is missing or empty`);
        assertNonEmptyString(tablePtr.table, `Table pointer 'table' field is missing or empty`);
        const ownerReference = this.resolveDatabase(tablePtr.database, contextDatabse);
        try {
            const value = V1_getRelation(ownerReference.value, tablePtr.schema, tablePtr.table);
            return createImplicitRelationReference(ownerReference, value);
        }
        catch (error) {
            if (allowImplicitToGeneratedDatabse &&
                ownerReference.value.includedStoreSpecifications.length > 0) {
                const generatedDatabase = ownerReference.value.includedStoreSpecifications.find((_s) => _s.generatedDatabase.schemas.find((_schema) => _schema.name === tablePtr.schema))?.generatedDatabase ??
                    guaranteeNonNullable(ownerReference.value.includedStoreSpecifications[0]
                        ?.generatedDatabase);
                const value = V1_getRelation(generatedDatabase, tablePtr.schema, tablePtr.table);
                // we want the reference to be of the table pointer database
                const ref = PackageableElementImplicitReference.create(generatedDatabase, tablePtr.database);
                return createImplicitRelationReference(ref, value);
            }
            throw error;
        }
    };
    resolveJoin = (joinPtr) => {
        assertNonEmptyString(joinPtr.db, `Join pointer 'db' field is missing or empty`);
        assertNonEmptyString(joinPtr.name, `Join pointer 'name' field is missing or empty`);
        const ownerReference = this.resolveDatabase(joinPtr.db);
        const value = getJoin(ownerReference.value, joinPtr.name);
        return JoinImplicitReference.create(ownerReference, value);
    };
    resolveFilter = (filterPtr) => {
        assertNonEmptyString(filterPtr.db, `Filter pointer 'db' field is missing or empty`);
        assertNonEmptyString(filterPtr.name, `Filter pointer 'name' field is missing or empty`);
        const ownerReference = this.resolveDatabase(filterPtr.db);
        const value = getFilter(ownerReference.value, filterPtr.name);
        return FilterImplicitReference.create(ownerReference, value);
    };
    resolveEnumValue = (enumeration, enumValue) => {
        const ownerReference = this.resolveEnumeration(enumeration);
        const value = getEnumValue(ownerReference.value, enumValue);
        return EnumValueImplicitReference.create(ownerReference, value);
    };
    resolveElement = (path, includePackage) => this.createImplicitPackageableElementReference(path, (_path) => this.graph.getElement(_path, includePackage));
    resolveType = (path) => this.createImplicitPackageableElementReference(path, this.graph.getType);
    resolveDataType = (path) => this.createImplicitPackageableElementReference(path, (_path) => guaranteeType(this.graph.getType(_path), DataType, `Can't find data type '${_path}'`));
    resolveProfile = (path) => this.createImplicitPackageableElementReference(path, this.graph.getProfile);
    resolveClass = (path) => this.createImplicitPackageableElementReference(path, this.graph.getClass);
    resolveEnumeration = (path) => this.createImplicitPackageableElementReference(path, this.graph.getEnumeration);
    resolveMeasure = (path) => this.createImplicitPackageableElementReference(path, this.graph.getMeasure);
    resolveUnit = (path) => this.createImplicitPackageableElementReference(path, this.graph.getUnit);
    resolveAssociation = (path) => this.createImplicitPackageableElementReference(path, this.graph.getAssociation);
    resolvePropertyOwner = (path) => this.createImplicitPackageableElementReference(path, this.graph.getPropertyOwner);
    resolveFunction = (path) => this.createImplicitPackageableElementReference(path, this.graph.getFunction);
    resolveStore = (path) => this.createImplicitPackageableElementReference(path, this.graph.getStore);
    resolveFlatDataStore = (path) => this.createImplicitPackageableElementReference(path, this.graph.getFlatDataStore);
    resolveDatabase = (path, generatedDatabasesInScope) => {
        const element = returnUndefOnError(() => this.createImplicitPackageableElementReference(path, this.graph.getDatabase));
        if (element) {
            return element;
        }
        if (generatedDatabasesInScope?.has(path)) {
            const generatedDb = generatedDatabasesInScope.get(path);
            if (generatedDb) {
                return PackageableElementImplicitReference.create(generatedDb, path);
            }
        }
        throw new AssertionError(`Can't find database ${path}`);
    };
    resolveMapping = (path) => this.createImplicitPackageableElementReference(path, this.graph.getMapping);
    resolveService = (path) => this.createImplicitPackageableElementReference(path, this.graph.getService);
    resolveConnection = (path) => this.createImplicitPackageableElementReference(path, this.graph.getConnection);
    resolveRuntime = (path) => this.createImplicitPackageableElementReference(path, this.graph.getRuntime);
    resolveGenerationSpecification = (path) => this.createImplicitPackageableElementReference(path, this.graph.getGenerationSpecification);
    resolveFileGeneration = (path) => this.createImplicitPackageableElementReference(path, this.graph.getFileGeneration);
    resolveDataElement = (path) => this.createImplicitPackageableElementReference(path, this.graph.getDataElement);
    resolveDataProduct = (path) => this.createImplicitPackageableElementReference(path, this.graph.getDataProduct);
}
export class V1_GraphBuilderContextBuilder {
    logService;
    /**
     * The (sub) graph where the current processing is taking place.
     * This information is important because each sub-graph holds their
     * own indexes for elements they are responsible for.
     *
     * e.g. dependency graph, generation graph, system graph, etc.
     */
    currentSubGraph;
    extensions;
    graph;
    sectionImports = [];
    section;
    options;
    constructor(graph, currentSubGraph, extensions, logService, options) {
        this.graph = graph;
        this.currentSubGraph = currentSubGraph;
        this.extensions = extensions;
        this.logService = logService;
        this.options = options;
    }
    withElement(element) {
        const section = this.graph.getOwnNullableSection(element.path);
        return this.withSection(section);
    }
    withSection(section) {
        this.section = section;
        if (section instanceof ImportAwareCodeSection) {
            this.sectionImports = this.sectionImports.concat(section.imports.map((i) => i.value));
        }
        this.sectionImports = uniq(this.sectionImports); // remove duplicates
        return this;
    }
    build() {
        return new V1_GraphBuilderContext(this);
    }
}
//# sourceMappingURL=V1_GraphBuilderContext.js.map