"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.getAdaptedSchema = getAdaptedSchema;
const common_1 = require("../../common");
const ux_specification_types_1 = require("@sap/ux-specification-types");
const decorators_1 = require("../../common/decoration/decorators");
const manifestPropertyUtils_1 = require("./manifestPropertyUtils");
const stableIdUtils_1 = require("./stableIdUtils");
const utils_1 = require("../utils");
const i18n_1 = require("../../../i18n/i18n");
/**
 * Checks whether a schema element contains an anyOf-property. If yes, all types directly or indirectly referred by one of the
 * branches of the anyOf are added to a collection of referred type names.
 *
 * @param element - the schema element to be processed
 * @param globalTypes - info about global types defined in the schema. Will possibly be extended by this function.
 */
function addAnyOfToReferredTypes(element, globalTypes) {
    if (element.anyOf) {
        element.anyOf.forEach(function (innerElement) {
            addReferredTypesFromElement(innerElement, globalTypes);
        });
    }
}
/**
 * Checks whether the items property of a given schema element is a single Definition. If yes, this Definition is returned.
 * In all other cases allowed by the type (boolean, array of BooleanOrDefinition, undefined) return undefined.
 * Note that ignoring arrays is justified by the fact that the schemas generated by Specification never have items pointing to arrays of DefinitionOrBoolean.
 *
 * @param element - the schema element to be checked
 * @returns the single Definition specified by the items-property of element resp. undefined
 */
function getSingleItem(element) {
    return typeof element.items !== 'object' || Array.isArray(element.items) ? undefined : element.items;
}
/**
 * Recursively analyzes a given schema element and adds the names of all types referred by it to the collection of referred type names.
 * Note that properties and items defined inline will not be considered.
 *
 * @param element - the schema element to be processed. Maybe faulty or boolean -> nothing to do.
 * @param globalTypes - info about global types defined in the schema. Will possibly be extended by this function.
 */
function addReferredTypesFromElement(element, globalTypes) {
    if (typeof element !== 'object') {
        // boolean or undefined -> no types referred here
        return;
    }
    // Now definition is actually an instance of Definition
    const refType = (0, common_1.getRefType)(element);
    if (refType) {
        addReferredTypesFromType(refType, globalTypes);
    }
    addAnyOfToReferredTypes(element, globalTypes);
    addReferredTypesFromElement(element.additionalProperties, globalTypes);
    // The schemas we are handling do not provide items as arrays directly:
    addReferredTypesFromElement(getSingleItem(element), globalTypes);
}
/**
 * Adds a globally defined type to the collection of referred type names and does the same for all its (directly or indirectly) referred types.
 * Note that this one (in contrast to addReferredTypesFromElement) will also observe properties defined inline and (if no properties are available)
 * items that are defined inline.
 *
 * @param type - Name of the globally defined type
 * @param globalTypes - info about global types defined in the schema. Will possibly be extended by this function.
 */
function addReferredTypesFromType(type, globalTypes) {
    if (globalTypes.referredTypeNames.has(type)) {
        return; // type already processed
    }
    globalTypes.referredTypeNames.add(type);
    const typeDefinition = globalTypes.types[type];
    addReferredTypesFromElement(typeDefinition, globalTypes);
    const properties = typeDefinition.properties;
    if (properties) {
        Object.values(properties).forEach((propertyDef) => addReferredTypesFromElement(propertyDef, globalTypes));
    }
    else {
        addReferredTypesFromElement(getSingleItem(typeDefinition), globalTypes);
    }
    addReferredTypesFromElement(typeDefinition.additionalProperties, globalTypes);
}
/**
 * Processes an element which will be added to the specific schema based on an AdaptationInfo.
 *
 * @param adaptationInfo - configures the process. If undefined the element should remain unchanged.
 * @param element - the schema element to be processed. Maybe faulty or boolean -> nothing to do.
 * @param generateParameters - generic parameters for schema generation
 * @param params - contains information specific for the current adjustment step. Property specificParams has been set by the previous processing level (see AdaptationInfo.specificParams).
 * @param globalTypes - info about global types defined in the schema. Will possibly be extended by this function.
 */
function applyAdaptationInfoToElement(adaptationInfo, element, generateParameters, params, globalTypes) {
    if (typeof element !== 'object') {
        // boolean or undefined -> no adaptation possible
        return;
    }
    // Now element is actually an instance of Definition
    // Two cases:
    // 1) element is specified by a global type via the #ref attribute
    // 2) element contains inline definition
    const refType = (0, common_1.getRefType)(element);
    if (refType) {
        // case 1
        // case 1a: global type is already defined in the generic schema and thus part of globalTypes.types
        // case 1b: global type is not yet defined in the generic schema. Then we expect its definition to be provided by adaptationInfo.
        let refTypeDefinition = globalTypes.types[refType];
        if (!refTypeDefinition) {
            // case 1b: Need to make type available globally
            refTypeDefinition = adaptationInfo?.refTypeDefinition;
            globalTypes.types[refType] = refTypeDefinition;
        }
        if (adaptationInfo) {
            let targetTypeDefinition;
            if (adaptationInfo.suffix) {
                // element should not point to the same global type as in the generic schema, but to an adapted type.
                // See jsdoc of property suffix for more details.
                const specificTypeName = `${refType}<${adaptationInfo.suffix}>`;
                element.$ref = (0, common_1.getDefinitionsRef)(specificTypeName);
                targetTypeDefinition = structuredClone(refTypeDefinition);
                // Add the newly defined type to the list of global types
                globalTypes.types[specificTypeName] = targetTypeDefinition;
                // and ensure that this type is not deleted as an unused type at the end of the process
                globalTypes.referredTypeNames.add(specificTypeName);
            }
            else {
                // The global type can be used unmodified
                globalTypes.referredTypeNames.add(refType);
                targetTypeDefinition = refTypeDefinition;
            }
            const newParams = {
                pageInfo: params.pageInfo,
                specificParams: adaptationInfo.specificParams || params.specificParams
            };
            // step down to the next hierarchy level
            adjustElement(generateParameters, newParams, adaptationInfo.syncRuleProvider, targetTypeDefinition, globalTypes);
        }
        else {
            // No adaptationInfo -> The subtree under the referred type will be taken over unchanged
            // Therefore, we simply need to add all types in this subtree to collection of referred type names to ensure that they are not removed from the schema at the end of the process
            addReferredTypesFromType(refType, globalTypes);
        }
    }
    else {
        // case 2: directly step down to the next hierarchy level
        adjustElement(generateParameters, params, adaptationInfo?.syncRuleProvider, element, globalTypes);
    }
}
/**
 * Helper function that transforms the given AccessorSpec into a JSONPath string.
 *
 * @param path - the path to the property
 * @param property - name of the property
 * @returns a JSONPath string identifying the access to the property
 */
function getJSONPathForProperty(path, property) {
    const pathIsArray = Array.isArray(path);
    let pathAsArray = pathIsArray ? path : path.path;
    if (pathIsArray || path.addTargetPropertyToPath) {
        pathAsArray = pathAsArray.concat([property]);
    }
    let jsonPath = '$';
    pathAsArray.forEach((segment) => {
        let previousSegment, currentSegment;
        if (typeof segment === 'number') {
            previousSegment = '[]';
            currentSegment = '' + segment;
        }
        else {
            currentSegment = segment;
        }
        jsonPath = (0, common_1.appendJsonPathSegment)(jsonPath, currentSegment, previousSegment);
    });
    return jsonPath;
}
/**
 * This function is the heart of the implementation of getAdaptedSchema (see below). It recursively traverses the passed schema element
 * and adjusts it according to the corresponding syncRule.
 * Note that there are two flavours of syncRules:
 * - syncRules valid for a class as a whole.
 * - syncRules valid for a specific property of a class.
 *
 * @param generateParameters - generic parameters for schema generation
 * @param params - contains information specific for the current adjustment step. Property specificParams has been set by the previous processing level(s) (see AdaptationInfo.specificParams).
 * @param syncRuleProvider - constructor of the class which has the syncRule for the current adjustment step
 * @param element - the section of the specific schema which is currently processed
 * Following properties of element will be analyzed for getting schemas on the next hierarchy level:
 * 'properties', 'items' (only if 'properties' is not there), 'additionalProperties'
 * @param globalTypes - info about global types defined in the schema. Will possibly be extended by this function.
 */
function adjustElement(generateParameters, params, syncRuleProvider, element, globalTypes) {
    const generalProcessingRule = { element };
    // First apply the syncRule of the class given by SyncRuleProvider.
    const generalSyncRule = (0, decorators_1.getReflectTypeMetadata)(syncRuleProvider);
    if (generalSyncRule?.processingRuleAdapter) {
        generalSyncRule.processingRuleAdapter(generalProcessingRule, params, generateParameters);
        if (!generalProcessingRule.element) {
            // syncRule has expressed that this subtree needs not to be processed.
            // This is added just for completeness: Normally, this will not be expressed on the type level but on the level of the property referring the type.
            return;
        }
    }
    // Now generalProcessingRule contains the information needed to process the content of element
    // First process the additionalProperties section of element (if required)
    applyAdaptationInfoToElement(generalProcessingRule.additionalPropertiesAdaptation, element.additionalProperties, generateParameters, params, globalTypes);
    // if generalProcessingRule has a more specific version of specificParams use it when processing the subtree
    const generalParams = {
        pageInfo: params.pageInfo,
        specificParams: generalProcessingRule.referenceAdaptation?.specificParams || params.specificParams
    };
    // Now process either the properties of element or (if properties is absent) the items.
    // In the second case we treat it as if element would have exactly one property (namely 'items') pointing to the content of element.items
    let properties = element.properties; // a map where the keys are the properties we want to handle and the values are the corresponding elements
    let propertyCollection; // the map which should finally be updated for all the properties we want to handle
    if (properties) {
        propertyCollection = properties;
    }
    else {
        const items = getSingleItem(element);
        if (items) {
            properties = {
                items
            };
            propertyCollection = element;
        }
        else {
            // Nothing to process -> we are done
            return;
        }
    }
    // Now we process the the prepared properties.
    // This will be done by an instance of type ProcessingRulesAdapter which is either taken from
    // generalProcessingRule.referenceAdaptation (dynamic case) or created on the fly from the syncRules defined for
    // the properties of the class which syncRuleProvider is a constructor for (static case).
    let syncRuleProviderInstance; // used for the static case. Instance of the class given by syncRuleProvider. Initialized on demand (i.e. for the first property).
    const processingRulesAdapter = generalProcessingRule.referenceAdaptation?.processingRulesAdapter || // the dynamic case
        function (property, processingRule) {
            // the static case
            syncRuleProviderInstance = syncRuleProviderInstance || (syncRuleProvider && new syncRuleProvider());
            const syncRule = (0, decorators_1.getReflectMetadata)(syncRuleProviderInstance, property);
            if (syncRule?.processingRuleAdapter) {
                syncRule.processingRuleAdapter(processingRule, generalParams, generateParameters);
            }
        };
    for (const property in properties) {
        const propertyDef = properties[property];
        if (typeof propertyDef !== 'object') {
            // For type safety handle the case that a property points to a boolean. Should actually not happen for schemas generated by the Specification.
            propertyCollection[property] = propertyDef;
            continue;
        }
        // Task of the next call is to modify propertyCopy (if needed) and return a ProcessingRule describing how this property should be processed further
        // Create a processingRule for the current property and let it be adapted by the processingRulesAdapter.
        const processingRule = { element: { ...propertyDef } }; // use a shallow copy of propertyDef in order to decouple from the original propertyDef
        processingRulesAdapter(property, processingRule);
        if (processingRule.element) {
            // the property should be part of the schema. Corresponding definition should be processingRule.element.
            // Apply generic changes on the element that have been specified by processingRulesAdapter:
            if (processingRule.controlId) {
                processingRule.element[ux_specification_types_1.SchemaTag.controlId] =
                    `${params.pageInfo.controlId}--${processingRule.controlId}`; // the processing rule only provides the (view-)local id -> make a global id out of it
            }
            else if (processingRule.manifestPath) {
                processingRule.element[ux_specification_types_1.SchemaTag.artifactType] = 'Manifest';
                processingRule.element[ux_specification_types_1.SchemaTag.manifestPath] = getJSONPathForProperty(processingRule.manifestPath, property);
            }
            // Now we are looking into the definition of the property itself and trigger the processing of the subtree
            addAnyOfToReferredTypes(processingRule.element, globalTypes);
            applyAdaptationInfoToElement(processingRule.referenceAdaptation, processingRule.element, generateParameters, generalParams, globalTypes);
            // In case the definition of the property possesses additionalProperties
            // (Note that additionalProperties defined on type level have already been processed above):
            applyAdaptationInfoToElement(processingRule.additionalPropertiesAdaptation, processingRule.element.additionalProperties, generateParameters, generalParams, globalTypes);
            // In case the definition of the property directly contains an items-element that needs to be processed further
            // (Note that items being defined on type level will be treated like a single property 'items' due to the logic above):
            applyAdaptationInfoToElement(processingRule.itemsAdaptation, getSingleItem(processingRule.element), generateParameters, generalParams, globalTypes);
            // Finally set processingRule.element as the new definition for the current property
            propertyCollection[property] = processingRule.element;
        }
        else {
            // if the element has been removed from the ProcessingRule this indicates that the property should be removed from the schema
            delete propertyCollection[property];
        }
    }
}
/**
 * Creates a page-specific schema for an FE v2 app page.
 * For this purpose it takes an instance of the generic schema for the page type.
 * The transformation into the specific schema is controlled by syncRules (in particular the property processingRuleAdapter).
 * The generated schema will contain information how its properties are mapped into manifest properties resp. flex changes.
 * - manifest properties contain properties `artifactType: "Manifest"` and `manifestPath: AccessorSpec`.
 *   In rare cases an additional non-standard mapping needs to be applied when moving values back and forth between manifest
 *   and configuration. For such cases a conversionExit specification is added to the schema as well.
 * - flexChanges contain properties `artifactType: "FlexChange"`, `controlId` and `controlType`.
 *
 * @param pageSpec - the page for which the schema should be generated
 * @param getGenericSchemaInfo - function providing base data for schema generation process (template component dependent)
 * @param logger - log information during schema generation process
 * @returns page-specific schema
 */
function getAdaptedSchema(pageSpec, getGenericSchemaInfo, logger) {
    (0, i18n_1.initI18n)(); // ensure that i18n texts can be resolved during the process
    const templateType = (0, common_1.getTemplateTypeFromManifest)(pageSpec.manifest, ux_specification_types_1.FioriElementsVersion.v2, logger);
    if (!templateType || templateType === ux_specification_types_1.TemplateType.OverviewPageV2) {
        return; // not supported (yet)
    }
    const { parent: pageDefinition } = (0, utils_1.getManifestPropertyByPath)(pageSpec.manifest, pageSpec.pagePath);
    if (!pageDefinition) {
        return; // no page definition found at the specified path
    }
    // Now we know that pageDefinition is an object. Check basic prerequisites on its content:
    const componentOfPage = pageDefinition['component'];
    if ((0, manifestPropertyUtils_1.hasPropertyTypeError)(pageDefinition, 'entitySet', logger, 'string', true) ||
        (0, manifestPropertyUtils_1.hasPropertyTypeError)(componentOfPage, 'name', logger, 'string', true)) {
        return; // not a valid page definition
    }
    const entitySet = pageDefinition['entitySet']; // type ensured above
    const componentName = componentOfPage['name']; // type ensured above
    const schemaGenerationInfo = getGenericSchemaInfo(componentName);
    const genericSchema = schemaGenerationInfo?.genericSchema;
    const syncRuleProvider = schemaGenerationInfo?.syncRuleProvider;
    if (!syncRuleProvider || !genericSchema) {
        return genericSchema; // no adaptation possible
    }
    const serviceAVT = pageSpec.parsedAnnotations || (0, common_1.parseAndMergeAndConvert)(pageSpec.annotations, logger);
    const entitySetDefinition = serviceAVT?.entitySets?.find((entitySetDefinition) => entitySetDefinition.name === entitySet);
    if (!entitySetDefinition) {
        return genericSchema; // not possible to adapt the schema if entity set is not defined
    }
    // Create a defensive copy of the generic schema that will be modified according to the sync rules and returned as result of this function
    const mySchema = structuredClone(genericSchema);
    const controlId = (0, stableIdUtils_1.getViewId)(pageSpec.appId, componentName, entitySet);
    const pageInfo = {
        appId: pageSpec.appId,
        componentName,
        entitySet,
        controlId,
        pagePath: pageSpec.pagePath
    };
    const params = {
        pageInfo
    };
    const generateParameters = {
        templateType,
        manifest: pageSpec.manifest,
        serviceAVT,
        fioriElementsVersion: ux_specification_types_1.FioriElementsVersion.v2,
        entitySet: entitySetDefinition,
        fragments: pageSpec.fragments,
        logger,
        entityType: entitySetDefinition.entityType
    };
    const globalTypes = {
        types: mySchema.definitions, // our generic schemas do not refer a type that is a boolean
        referredTypeNames: new Set()
    };
    // Now adjust mySchema based on the input parameters.
    // Note that this will also ensure that globalTypes.referredTypeNames is the collection of global type names that are referred within the schema.
    adjustElement(generateParameters, params, syncRuleProvider, mySchema, globalTypes);
    // Remove all global type definitions which are not used within the generated schema.
    for (const typeName in mySchema.definitions) {
        if (!globalTypes.referredTypeNames.has(typeName)) {
            delete mySchema.definitions[typeName];
        }
    }
    return mySchema;
}
//# sourceMappingURL=schemaAdaptation.js.map