"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.exportXMLFragmentConfig = exports.exportFPMCustomPageConfig = void 0;
exports.getExtensionExportResult = getExtensionExportResult;
const xml_js_1 = require("xml-js");
const ux_specification_types_1 = require("@sap/ux-specification-types");
const utils_1 = require("../utils/utils");
const macros_1 = require("../utils/macros");
const utils_2 = require("../../common/utils");
/**
 * Method compares values if values are different after converted to string.
 * It is used to determine if we need make change in XML file.
 *
 * @param {unknown} value1 First value to compare.
 * @param {unknown} value2 Second value to compare.
 * @returns {boolean} Returns true if values are same after values converted to string.
 */
function isExportValueEquals(value1, value2) {
    return value1 !== undefined && value2 !== undefined ? `${value1}` === `${value2}` : value1 === value2;
}
/**
 * Method deletes property by emoving attribute from element.
 * Method also considers if it is last attribute and element is complex property, then element would removed as well.
 *
 * @param {TraverseData} traverseData Schema and XML traverse data.
 * @param {string} name Name of property.
 */
function deleteProperty(traverseData, name) {
    const { element, schema, parent, contextPath } = traverseData;
    delete element.attributes[name];
    if (schema.metadata?.type === ux_specification_types_1.MacrosPropertyType.Property &&
        Object.keys(element.attributes).length === 0 &&
        parent?.element.elements) {
        // Last attribute is deleted for property - if element is property then remove property itself
        const context = contextPath[contextPath.length - 1];
        if (context) {
            parent.element.elements.splice(context.index, 1);
        }
    }
}
/**
 * Resolves a namespace-prefixed element name from a schema $ref and the root element's xmlns declarations.
 * Looks for an exact xmlns match first, then derives a prefix from the closest parent namespace
 * (e.g., xmlns:macros="sap.fe.macros" → "macrosTable" for "sap.fe.macros.table") and adds the declaration.
 *
 * @param {string} name Element name to prefix.
 * @param {string} $ref Schema $ref (e.g., "#/definitions/sap.fe.macros.table.MassEdit").
 * @param {Element} rootElement Root XML element containing xmlns declarations.
 * @returns {string} Prefixed element name, or original name if no prefix resolved.
 */
function resolvePrefixedName(name, $ref, rootElement) {
    const definitionKey = (0, utils_2.getDefinitionKey)($ref);
    const lastDot = definitionKey.lastIndexOf('.');
    if (lastDot <= 0) {
        return name;
    }
    const namespace = definitionKey.slice(0, lastDot);
    const attrs = rootElement.attributes || {};
    // Find existing xmlns declaration for this namespace
    for (const [attr, value] of Object.entries(attrs)) {
        if (value === namespace) {
            return attr === 'xmlns' ? name : `${attr.slice(6)}:${name}`;
        }
    }
    // Derive prefix from closest parent namespace and add declaration
    let bestPrefix = '';
    let bestNs = '';
    for (const [attr, value] of Object.entries(attrs)) {
        if (attr.startsWith('xmlns:') &&
            typeof value === 'string' &&
            namespace.startsWith(value + '.') &&
            value.length > bestNs.length) {
            bestNs = value;
            bestPrefix = attr.slice(6);
        }
    }
    if (bestNs) {
        const suffix = namespace.slice(bestNs.length + 1);
        const derivedPrefix = bestPrefix + suffix.charAt(0).toUpperCase() + suffix.slice(1);
        rootElement.attributes[`xmlns:${derivedPrefix}`] = namespace;
        return `${derivedPrefix}:${name}`;
    }
    return name;
}
/**
 * Callback method of traversing schema and XML.
 * Method exports value from page config object to XML element.
 *
 * @param {TraverseData} traverseData Schema and XML traverse data.
 * @param {SchemaDefinition} property Schema property to use for export.
 * @param {object} config Fragment of page config object.
 * @param {string} name Name of property to export.
 * @returns {boolean} Is new value differs.
 */
function detectElementDeletion(traverseData, property, config, name) {
    const { element, parentElement } = traverseData;
    if (element?.name === name) {
        if (traverseData.data?.nodesToDelete) {
            traverseData.data.nodesToDelete.push({
                element,
                parent: parentElement
            });
        }
        return true;
    }
    return false;
}
/**
 * Callback method of traversing schema and XML.
 * Method exports value from page config object to XML element.
 *
 * @param {TraverseData} traverseData Schema and XML traverse data.
 * @param {SchemaDefinition} property Schema property to use for export.
 * @param {object} config Fragment of page config object.
 * @param {string} name Name of property to export.
 * @returns {boolean} Is new value differs.
 */
function exportProperty(traverseData, property, config, name) {
    if (property.hidden) {
        return false;
    }
    const { element } = traverseData;
    if (name in config && config[name] !== undefined) {
        if (property.isViewNode) {
            return exportViewNode(traverseData, property, config, name);
        }
        if (element && !isExportValueEquals(element.attributes?.[name], config[name])) {
            exportNonViewAttribute(element, name, config, property, traverseData);
            return true;
        }
    }
    else if (property.isViewNode && property.metadata?.overrideClass) {
        return exportViewNode(traverseData, property, config, name);
    }
    else if (element?.attributes?.[name]) {
        deleteProperty(traverseData, name);
        return true;
    }
    return false;
}
/**
 * Dispatches export of an `isViewNode` property to the appropriate handler based on whether
 * the schema carries an `overrideClass` (annotation-based override) or is a standard view node.
 *
 * @param traverseData Schema and XML traverse context.
 * @param property Schema definition of the property being exported.
 * @param config Fragment of the page config object containing the current value.
 * @param name Property name, used as the XML element name or override key.
 * @returns Whether the config value was written to XML.
 */
function exportViewNode(traverseData, property, config, name) {
    if (!traverseData.element.elements) {
        traverseData.element.elements = [];
    }
    return property.metadata?.overrideClass && traverseData.data?.rootElement
        ? exportOverrideNode(traverseData, property, config, name)
        : exportStandardViewNode(traverseData, property, config, name);
}
/**
 * Exports an annotation-based view node (e.g. a column with a `ColumnOverride`) by finding or
 * creating a typed child element (e.g. `<macrosTable:ColumnOverride key="..."/>`), then
 * recursively traversing its schema to write the override attributes.
 *
 * @param traverseData Schema and XML traverse context.
 * @param property Schema definition carrying `metadata.overrideClass` and the override properties.
 * @param config Fragment of the page config object containing the override values.
 * @param name Property name used as both the `key` attribute on the override element and the config key.
 * @returns Whether any attributes were written to the override element.
 */
function exportOverrideNode(traverseData, property, config, name) {
    const { element, fullSchema, contextPath } = traverseData;
    const overrideClassName = property.metadata.overrideClass;
    const shortName = overrideClassName.slice(overrideClassName.lastIndexOf('.') + 1);
    const prefixedName = resolvePrefixedName(shortName, `#/definitions/${overrideClassName}`, traverseData.data.rootElement);
    // Reuse existing element with matching key rather than creating a duplicate
    let targetElement = (0, macros_1.findChildElementByLocalNameAndKey)(element, shortName, name);
    if (!targetElement) {
        targetElement = { name: prefixedName, type: 'element', attributes: {} };
        element.elements.push(targetElement);
    }
    // 'key' is a structural identifier, not a user-configurable property — set it directly
    // so it is always present without being part of the schema-driven traversal.
    targetElement.attributes = targetElement.attributes ?? {};
    targetElement.attributes.key = name;
    const result = (0, macros_1.traverseSchema)({
        fullSchema,
        schema: property,
        element: targetElement,
        contextPath: [...contextPath, { name, index: 0 }],
        config: config[name] ?? {},
        parent: traverseData,
        parentElement: element,
        data: traverseData.data
    }, exportProperty, detectElementDeletion);
    // If no override attributes remain (only 'key'), the override is empty — remove the element
    const overrideAttrs = Object.keys(targetElement.attributes ?? {}).filter((k) => k !== 'key');
    if (overrideAttrs.length === 0 && element.elements) {
        const idx = element.elements.indexOf(targetElement);
        if (idx !== -1) {
            element.elements.splice(idx, 1);
        }
    }
    return result;
}
/**
 * Exports a standard view node by creating a plain named child element and recursively
 * traversing its schema to write nested properties.
 *
 * @param traverseData Schema and XML traverse context.
 * @param property Schema definition of the view node property.
 * @param config Fragment of the page config object containing the nested config.
 * @param name Property name used as the XML element name.
 * @returns Whether any nested properties were written.
 */
function exportStandardViewNode(traverseData, property, config, name) {
    const { element, fullSchema, contextPath } = traverseData;
    const targetElement = { name, type: 'element' };
    element.elements.push(targetElement);
    return (0, macros_1.traverseSchema)({
        fullSchema,
        schema: property,
        element: targetElement,
        contextPath: [...contextPath, { name, index: 0 }],
        config: config[name],
        parent: traverseData,
        parentElement: element,
        data: traverseData.data
    }, exportProperty, detectElementDeletion);
}
/**
 * Exports a non-view-node property value to XML. Object values become child elements;
 * single-cardinality aggregations produce a wrapper + inner control structure; primitives
 * are written as attributes on the current element.
 *
 * @param element Current XML element to write into.
 * @param name Property name used as the attribute or child element name.
 * @param config Fragment of the page config object containing the current value.
 * @param property Schema definition, used to detect single-cardinality aggregations.
 * @param traverseData Full traverse context, needed to resolve namespace prefixes.
 */
function exportNonViewAttribute(element, name, config, property, traverseData) {
    const value = config[name];
    if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
        element.elements = element.elements || [];
        const additionalProperties = typeof property.additionalProperties === 'object' ? property.additionalProperties : {};
        const isSingleAggregation = property.metadata?.aggregationCardinality === ux_specification_types_1.MacrosAggregatioCardinality.Single;
        if (isSingleAggregation && '$ref' in additionalProperties && traverseData.data?.rootElement) {
            exportSingleAggregation(element, name, value, additionalProperties, traverseData.data.rootElement);
        }
        else {
            element.elements.push({ name, type: 'element', attributes: value });
        }
    }
    else {
        element.attributes = element.attributes || {};
        element.attributes[name] = value;
    }
}
/**
 * Writes a single-cardinality aggregation as a namespace-prefixed wrapper element containing
 * one inner control element with the primitive config attributes.
 *
 * @param element Parent XML element to append the aggregation structure to.
 * @param name Aggregation property name, used to derive the wrapper element name.
 * @param value Config object holding the control's attribute values.
 * @param additionalProperties Schema `additionalProperties` containing the `$ref` to the control class.
 * @param additionalProperties.$ref Reference to the control class definition used for namespace resolution.
 * @param rootElement Root XML element used for namespace prefix resolution.
 */
function exportSingleAggregation(element, name, value, additionalProperties, rootElement) {
    // Single-cardinality aggregations produce a wrapper + inner control:
    //   <macros:massEdit>          (aggregation — inherits parent's prefix)
    //     <macrosTable:MassEdit/>   (control — prefix from $ref namespace)
    //   </macros:massEdit>
    const aggregationRef = additionalProperties.$ref;
    const colonIdx = element.name?.indexOf(':') ?? -1;
    const aggregationName = colonIdx > 0 ? `${element.name.slice(0, colonIdx)}:${name}` : name;
    const definitionKey = (0, utils_2.getDefinitionKey)(aggregationRef);
    const className = definitionKey.slice(definitionKey.lastIndexOf('.') + 1);
    const controlName = resolvePrefixedName(className, aggregationRef, rootElement);
    // Config may be nested as { ClassName: { attrs } } (from previous import) or flat { attrs }
    const innerConfig = value[className] ?? value;
    // XML attributes must be primitives — strip nested objects (sub-aggregations)
    const controlAttrs = {};
    for (const [key, val] of Object.entries(innerConfig)) {
        if (typeof val !== 'object' || val === null) {
            controlAttrs[key] = val;
        }
    }
    element.elements.push({
        name: aggregationName,
        type: 'element',
        elements: [{ name: controlName, type: 'element', attributes: controlAttrs }]
    });
}
/**
 * Deletes elements from their parent containers.
 *
 * @param nodesToDelete Array of elements marked for deletion with their parent references
 */
function deleteElements(nodesToDelete) {
    for (const deleteNode of nodesToDelete) {
        const { parent, element } = deleteNode;
        if (!parent?.elements) {
            continue;
        }
        const index = parent.elements.findIndex((el) => el === element);
        if (index !== -1) {
            parent.elements.splice(index, 1);
        }
    }
}
/**
 * Populates export results with data from extension.
 *
 * @param {ExtensionFileData} extensionData Extension data from parsed XML file.
 * @param {ExportResults} exportResults Export results API parameters needed for the export.
 * @param {object} config Extension of page config object.
 * @param {SchemaDefinition} jsonSchema Extesion definition schema.
 * @param {string} extensionType Extension type - view or fragment.
 * @param {ExtensionLogger | undefined} logger Logger class for logging messages.
 */
function getExtensionExportResult(extensionData, exportResults, config, jsonSchema, extensionType, logger) {
    const element = (0, macros_1.parseXML)(extensionData.file, false, logger);
    if (element) {
        const coreElement = (0, macros_1.getRootElement)(element);
        const traversionData = {
            nodesToDelete: [],
            rootElement: coreElement
        };
        const differs = (0, macros_1.traverseSchema)({
            fullSchema: jsonSchema,
            schema: jsonSchema,
            element: coreElement,
            contextPath: [],
            config,
            data: traversionData
        }, exportProperty, detectElementDeletion);
        if (differs) {
            if (traversionData.nodesToDelete?.length) {
                deleteElements(traversionData.nodesToDelete);
            }
            const fileContent = (0, xml_js_1.js2xml)(element, { spaces: 4 });
            const extensionExportResult = {
                dataSourceUri: extensionData.file.dataSourceUri,
                fileContent,
                changeIndicator: ux_specification_types_1.ChangeIndicator.Updated
            };
            if (extensionType === 'view') {
                exportResults.views = [extensionExportResult];
            }
            else if (extensionType === 'fragment') {
                exportResults.fragments.push(extensionExportResult);
            }
        }
    }
}
/**
 * Run through the given FPM Custom Page config and return respective XML View result.
 * If no changes in config, then view is not returned.
 *
 * @param exportParameters All API parameters needed for the export.
 * @param defaultExportResult Default export result.
 * @returns Object comprising the updated xml view
 */
const exportFPMCustomPageConfig = (exportParameters, defaultExportResult) => {
    const { views, manifest, jsonSchema, page, logger } = exportParameters;
    const exportResults = { ...defaultExportResult, manifest };
    let viewData = undefined;
    if (jsonSchema && Object.keys(jsonSchema).length > 0) {
        viewData = (0, utils_1.getPageCustomViewFile)(views || [], page.name, manifest, logger);
    }
    if (viewData) {
        getExtensionExportResult(viewData, exportResults, page.config, jsonSchema, 'view', logger);
    }
    return exportResults;
};
exports.exportFPMCustomPageConfig = exportFPMCustomPageConfig;
/**
 * Runs through the given config and pushes changed fragments into export results.
 *
 * @param {TransferParameterTypeV4} transferParameters List of transfer parameters passed from API.
 * @param {string} fragmentName Full name of the fragment.
 * @param {object} config Current (sub)object of the configuration file.
 */
const exportXMLFragmentConfig = (transferParameters, fragmentName, config) => {
    const { appSchema, exportResults, fragments, pageNameArray, logger } = transferParameters;
    const { manifest } = exportResults;
    let fragmentData = undefined;
    const extensionType = 'fragment';
    if (appSchema && Object.keys(appSchema).length > 0) {
        const targetManifestPage = (0, utils_1.getManifestPage)(manifest, pageNameArray[0]);
        fragmentData = (0, utils_1.getPageCustomExtensionFile)(fragments, targetManifestPage, manifest, extensionType, fragmentName);
    }
    if (fragmentData) {
        const definitionKey = (0, macros_1.getCustomExtensionFragmentDefinitionKey)(fragmentName);
        const targetConfigSchema = appSchema['definitions'][definitionKey];
        const tempSchema = {
            ...targetConfigSchema,
            definitions: appSchema['definitions']
        };
        getExtensionExportResult(fragmentData, exportResults, config, tempSchema, extensionType, logger);
    }
};
exports.exportXMLFragmentConfig = exportXMLFragmentConfig;
//# sourceMappingURL=fpmCustomPage.js.map