"use strict";
/*---------------------------------------------------------------------------------------------
 *  Copyright (c) Microsoft Corporation. All rights reserved.
 *  Licensed under the MIT License. See License.txt in the project root for license information.
 *--------------------------------------------------------------------------------------------*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.createInlinedPropertiesPlugin = exports.createVirtuals = exports.singularize = void 0;
const codemodel_1 = require("@autorest/codemodel");
const codegen_1 = require("@azure-tools/codegen");
const linq_1 = require("@azure-tools/linq");
const command_operation_1 = require("../utils/command-operation");
const model_state_1 = require("../utils/model-state");
const schema_1 = require("../utils/schema");
const resolve_conflicts_1 = require("../utils/resolve-conflicts");
const command_operation_2 = require("../utils/command-operation");
function getPluralizationService() {
    const result = new codegen_1.EnglishPluralizationService();
    result.addWord('Database', 'Databases');
    result.addWord('database', 'databases');
    result.addWord('Premise', 'Premises');
    result.addWord('premise', 'premises');
    return result;
}
function singularize(word) {
    return getPluralizationService().singularize(word);
}
exports.singularize = singularize;
function getCombinedDescription(rawDescription, externalDocsUrl) {
    let description = rawDescription !== null && rawDescription !== void 0 ? rawDescription : '';
    if (!!externalDocsUrl && !!externalDocsUrl.trim()) {
        description = description.concat(` Please visit external url ${externalDocsUrl} to get more information.`);
    }
    return description;
}
function getNameOptions(typeName, components) {
    const result = new Set();
    // add a variant for each incrementally inclusive parent naming scheme.
    for (let i = 0; i < (0, linq_1.length)(components); i++) {
        const subset = (0, codegen_1.pascalCase)([...(0, codegen_1.removeSequentialDuplicates)(components.slice(-1 * i, (0, linq_1.length)(components)))]);
        result.add(subset);
    }
    // add a second-to-last-ditch option as <typename>.<name>
    result.add((0, codegen_1.pascalCase)([...(0, codegen_1.removeSequentialDuplicates)([...(0, codegen_1.fixLeadingNumber)((0, codegen_1.deconstruct)(typeName)), ...(0, codegen_1.deconstruct)(components.last)])]));
    return [...result.values()];
}
function createVirtualProperties(schema, stack, threshold, conflicts) {
    var _a, _b, _c, _d, _e, _f, _g;
    // Some properties should be removed are wrongly kept as null and need to clean them
    if (schema.properties) {
        schema.properties = schema.properties.filter(each => each);
    }
    // dolauli
    //    owned: all properties(obj & nonobj) in the schema,
    //  inherited: Properties from parents,
    //    inlined: for obj properties, flatten them to children,
    // did we already inline this object
    if (schema.language.default.inline === 'yes') {
        return true;
    }
    if (schema.language.default.inline === 'no') {
        return false;
    }
    // this is bad. This would happen when we have a circular reference in the tree.
    // dolauli curious in which case this will happen, got it to use no-inline to skip inline and avoid circular reference
    if (schema.language.default.inline === 'inprogress') {
        let text = (`Note: during processing of '${schema.language.default.name}' a circular reference has been discovered.`);
        text += '\n  In order to proceed, you must add a directive to indicate which model you want to not inline.\n';
        text += '\ndirective:';
        text += '\n- no-inline:  # choose ONE of these models to disable inlining';
        for (const each of stack) {
            text += (`\n  - ${each} `);
        }
        text += '\n';
        conflicts.push(text);
        /*     `directive:
             - no-inline:
               - MyModel
               - YourModel
               - HerModel
            ` */
        // `, and we're skipping inlining.\n  ${stack.join(' => ')}`);
        // mark it as 'not-inlining'
        schema.language.default.inline = 'no';
        return false;
    }
    // ok, set to in progress now.
    schema.language.default.inline = 'inprogress';
    // virutual property set.
    const virtualProperties = schema.language.default.virtualProperties = {
        owned: new Array(),
        inherited: new Array(),
        inlined: new Array(),
    };
    // First we should run thru the properties in parent classes and create inliners for each property they have.
    // dolauli handle properties in parents
    for (const parentSchema of (0, linq_1.values)((_a = schema.parents) === null || _a === void 0 ? void 0 : _a.immediate)) {
        // make sure that the parent is done.
        // Guess parent should always be an object.
        if (!(0, codemodel_1.isObjectSchema)(parentSchema))
            continue;
        createVirtualProperties(parentSchema, [...stack, `${schema.language.default.name}`], threshold, conflicts);
        const parentProperties = parentSchema.language.default.virtualProperties || {
            owned: [],
            inherited: [],
            inlined: [],
        };
        // now we go thru the parent's virutal properties and create our own copies 
        for (const virtualProperty of [...parentProperties.inherited, ...parentProperties.inlined, ...parentProperties.owned]) {
            // make sure that we have a list of shared owners of this property.
            virtualProperty.sharedWith = virtualProperty.sharedWith || [virtualProperty];
            // we are just copying over theirs to ours.
            const inheritedProperty = {
                name: virtualProperty.name,
                property: virtualProperty.property,
                private: virtualProperty.private,
                nameComponents: virtualProperty.nameComponents,
                nameOptions: virtualProperty.nameOptions,
                accessViaProperty: virtualProperty,
                accessViaMember: virtualProperty,
                accessViaSchema: parentSchema,
                originalContainingSchema: virtualProperty.originalContainingSchema,
                description: virtualProperty.description,
                alias: [],
                create: virtualProperty.create,
                update: virtualProperty.update,
                read: virtualProperty.read,
                readOnly: virtualProperty.readOnly,
                required: virtualProperty.required,
                sharedWith: virtualProperty.sharedWith,
            };
            // add it to the list of virtual properties that share this property.
            virtualProperty.sharedWith.push(inheritedProperty);
            // add it to this class.
            virtualProperties.inherited.push(inheritedProperty);
        }
    }
    // dolauli figure out object properties and non object properties in this class 
    const [objectProperties, nonObjectProperties] = (0, linq_1.values)(schema.properties).bifurcate(each => !schema.language.default['skip-inline'] && // if this schema is marked skip-inline, none can be inlined, treat them all as straight properties.
        !each.schema.language.default['skip-inline'] && // if the property schema is marked skip-inline, then it should not be processed either.
        each.schema.type === codemodel_1.SchemaType.Object && // is it an object 
        (0, schema_1.getAllProperties)(each.schema).length > 0 // does it have properties (or inherit properties)
    );
    // run thru the properties in this class.
    // dolauli handle properties in this class
    for (const property of objectProperties) {
        const mutability = (0, schema_1.getMutability)(property);
        const propertyName = property.language.default.name;
        // for each object member, make sure that it's inlined it's children that it can.
        createVirtualProperties(property.schema, [...stack, `${schema.language.default.name}`], threshold, conflicts);
        // this happens if there is a circular reference.
        // this means that this class should not attempt any inlining of that property at all .
        // dolauli pay attention to the condition check
        const isDict = property.schema.type === codemodel_1.SchemaType.Dictionary || ((_c = (_b = property.schema.parents) === null || _b === void 0 ? void 0 : _b.immediate) === null || _c === void 0 ? void 0 : _c.find((s) => s.type === codemodel_1.SchemaType.Dictionary));
        const canInline = (!property.schema.language.default['skip-inline']) &&
            (!property.schema.language.default.byReference) &&
            (!isDict) &&
            property.schema.language.default.inline === 'yes';
        // the target has properties that we can inline
        const virtualChildProperties = property.schema.language.default.virtualProperties || {
            owned: [],
            inherited: [],
            inlined: [],
        };
        const allNotRequired = (0, linq_1.values)((0, schema_1.getAllPublicVirtualProperties)()).all(each => !each.property.language.default.required);
        const childCount = (0, linq_1.length)(virtualChildProperties.owned) + (0, linq_1.length)(virtualChildProperties.inherited) + (0, linq_1.length)(virtualChildProperties.inlined);
        if (canInline && (property.language.default.required || allNotRequired) && (childCount < threshold || propertyName === 'properties')) {
            // if the child property is low enough (or it's 'properties'), let's create virtual properties for each one.
            // create a private property for the inlined ones to use.
            const combinedDescription = getCombinedDescription(property.language.default.description, (_e = (_d = property.schema) === null || _d === void 0 ? void 0 : _d.externalDocs) === null || _e === void 0 ? void 0 : _e.url);
            property.language.default.description = combinedDescription;
            const privateProperty = {
                name: (0, codegen_1.getPascalIdentifier)(propertyName),
                propertySchema: schema,
                property,
                nameComponents: [(0, codegen_1.getPascalIdentifier)(propertyName)],
                nameOptions: getNameOptions(schema.language.default.name, [propertyName]),
                private: true,
                description: property.summary || '',
                originalContainingSchema: schema,
                alias: [],
                required: property.required || property.language.default.required,
            };
            virtualProperties.owned.push(privateProperty);
            for (const inlinedProperty of [...virtualChildProperties.inherited, ...virtualChildProperties.owned]) {
                // child properties are be inlined without prefixing the name with the property name
                // unless there is a collision, in which case, we have to resolve 
                // (scan back from the far right)
                // deeper child properties should be inlined with their parent's name 
                // ie, this.[properties].owner.name should be this.ownerName 
                const proposedName = (0, codegen_1.getPascalIdentifier)(`${propertyName === 'properties' || /*objectProperties.length === 1*/ propertyName === 'error' ? '' : (0, codegen_1.pascalCase)((0, codegen_1.fixLeadingNumber)((0, codegen_1.deconstruct)(propertyName)).map(each => singularize(each)))} ${inlinedProperty.name}`);
                const components = [...(0, codegen_1.removeSequentialDuplicates)([propertyName, ...inlinedProperty.nameComponents])];
                let readonly = inlinedProperty.readOnly || property.readOnly;
                const create = mutability.create && inlinedProperty.create && !readonly;
                const update = mutability.update && inlinedProperty.update && !readonly;
                const read = mutability.read && inlinedProperty.read;
                readonly = readonly || (read && !update && !create);
                virtualProperties.inlined.push({
                    name: proposedName,
                    property: inlinedProperty.property,
                    private: inlinedProperty.private,
                    nameComponents: components,
                    nameOptions: getNameOptions(inlinedProperty.property.schema.language.default.name, components),
                    accessViaProperty: privateProperty,
                    accessViaMember: inlinedProperty,
                    accessViaSchema: schema,
                    originalContainingSchema: schema,
                    description: inlinedProperty.description,
                    alias: [],
                    create: create,
                    update: update,
                    read: read,
                    readOnly: readonly,
                    required: inlinedProperty.required && privateProperty.required,
                });
            }
            for (const inlinedProperty of [...virtualChildProperties.inlined]) {
                // child properties are be inlined without prefixing the name with the property name
                // unless there is a collision, in which case, we have to resolve 
                // (scan back from the far right)
                // deeper child properties should be inlined with their parent's name 
                // ie, this.[properties].owner.name should be this.ownerName 
                const proposedName = (0, codegen_1.getPascalIdentifier)(inlinedProperty.name);
                let readonly = inlinedProperty.readOnly || property.readOnly;
                const create = mutability.create && inlinedProperty.create && !readonly;
                const update = mutability.update && inlinedProperty.update && !readonly;
                const read = mutability.read && inlinedProperty.read;
                readonly = readonly || (read && !update && !create);
                const components = [...(0, codegen_1.removeSequentialDuplicates)([propertyName, ...inlinedProperty.nameComponents])];
                virtualProperties.inlined.push({
                    name: proposedName,
                    property: inlinedProperty.property,
                    private: inlinedProperty.private,
                    nameComponents: components,
                    nameOptions: getNameOptions(inlinedProperty.property.schema.language.default.name, components),
                    accessViaProperty: privateProperty,
                    accessViaMember: inlinedProperty,
                    accessViaSchema: schema,
                    originalContainingSchema: schema,
                    description: inlinedProperty.description,
                    alias: [],
                    create: create,
                    update: update,
                    read: read,
                    readOnly: readonly,
                    required: inlinedProperty.required && privateProperty.required
                });
            }
        }
        else {
            // otherwise, we're not below the threshold, and we should treat this as a non-inlined property
            nonObjectProperties.push(property);
        }
    }
    for (const property of nonObjectProperties) {
        const name = (0, codegen_1.getPascalIdentifier)(property.language.default.name);
        // this is not something that has properties,
        // so we don't need to do any inlining
        // however, we can add it to our list of virtual properties
        // so that our consumers can get it.
        const mutability = (0, schema_1.getMutability)(property);
        const combinedDescription = getCombinedDescription(property.language.default.description, (_g = (_f = property.schema) === null || _f === void 0 ? void 0 : _f.externalDocs) === null || _g === void 0 ? void 0 : _g.url);
        property.language.default.description = combinedDescription;
        virtualProperties.owned.push({
            name,
            property,
            nameComponents: [name],
            nameOptions: [name],
            description: property.summary || '',
            originalContainingSchema: schema,
            alias: [],
            create: mutability.create && !property.readOnly,
            update: mutability.update && !property.readOnly,
            read: mutability.read,
            readOnly: property.readOnly || (mutability.read && !mutability.create && !mutability.update),
            required: property.required || property.language.default.required
        });
    }
    // resolve name collisions.
    const allProps = [...virtualProperties.owned, ...virtualProperties.inherited, ...virtualProperties.inlined];
    const inlined = new Map();
    for (const each of allProps) {
        // track number of instances of a given name.
        inlined.set(each.name, (inlined.get(each.name) || 0) + 1);
    }
    const usedNames = new Set(inlined.keys());
    for (const each of virtualProperties.inlined.sort((a, b) => (0, linq_1.length)(a.nameOptions) - (0, linq_1.length)(b.nameOptions))) {
        const ct = inlined.get(each.name);
        if (ct && ct > 1) {
            // console.error(`Fixing collision on name ${each.name} #${ct} `);
            each.name = (0, codegen_1.selectName)(each.nameOptions, usedNames);
        }
    }
    schema.language.default.inline = 'yes';
    return true;
}
function createVirtualParameters(operation) {
    var _a;
    // dolauli expand body parameter
    // for virtual parameters, there are two keys, operation and body
    const virtualParameters = {
        operation: new Array(),
        body: new Array()
    };
    const dropBodyParameter = !!operation.details.default.dropBodyParameter;
    // loop thru the parameters of the command operation, and if there is a body parameter, expand it if necessary.
    for (const parameter of (0, linq_1.values)(operation.parameters)) {
        if (parameter.details.default.constantValue) {
            // this parameter has a constant value -- SKIP IT
            continue;
        }
        // dolauli fromhost and apiversion are not exposed, this if block looks useless
        if (parameter.details.default.fromHost || parameter.details.default.apiversion) {
            // handled in the generator right now. Not exposed to the user directly.
            continue;
        }
        if (dropBodyParameter && parameter.details.default.isBodyParameter) {
            // the client will make a hidden body parameter for this, and we're expected to fill it.
            const vps = parameter.schema.language.default.virtualProperties;
            if (vps) {
                for (const virtualProperty of [...vps.inherited, ...vps.owned, ...vps.inlined]) {
                    // dolauli add virtual parameter for virtual property
                    if (virtualProperty.private || virtualProperty.readOnly || virtualProperty.property.readOnly || virtualProperty.property.language.default.constantValue !== undefined || virtualProperty.property.language.default.HeaderProperty === 'Header') {
                        // private or readonly properties aren't needed as parameters. 
                        continue;
                    }
                    // Add support for x-ms-mutability
                    if (operation.operationType === command_operation_2.OperationType.Create && !virtualProperty.create) {
                        continue;
                    }
                    else if (operation.operationType === command_operation_2.OperationType.Update && !virtualProperty.update) {
                        continue;
                    }
                    virtualParameters.body.push({
                        name: virtualProperty.name,
                        description: virtualProperty.property.language.default.description,
                        nameOptions: virtualProperty.nameOptions,
                        required: parameter.required ? shouldBeRequired(operation, virtualProperty) : false,
                        schema: virtualProperty.property.schema,
                        origin: virtualProperty,
                        alias: []
                    });
                }
            }
        }
        else {
            // dolauli if not drop body or not body parameter add it to operation 
            const combinedDescription = getCombinedDescription(parameter.schema.language.default.description, (_a = parameter.schema.externalDocs) === null || _a === void 0 ? void 0 : _a.url);
            parameter.schema.language.default.description = combinedDescription;
            virtualParameters.operation.push({
                name: parameter.details.default.name,
                nameOptions: [parameter.details.default.name],
                description: parameter.details.default.description,
                required: parameter.details.default.isBodyParameter ? true : parameter.required,
                schema: parameter.schema,
                origin: parameter,
                alias: []
            });
        }
    }
    (0, resolve_conflicts_1.resolveParameterNames)([], virtualParameters);
    // dolauli see operation.details.default.virtualParameters
    operation.details.default.virtualParameters = virtualParameters;
}
async function createVirtuals(state) {
    /*
      A model class should provide inlined properties for anything in a property called properties
      
      Classes that have $THRESHOLD number of properties should be inlined.
   
      Individual models can change the $THRESHOLD for generate
    */
    const threshold = await state.getValue('inlining-threshold', 24);
    const conflicts = new Array();
    for (const schema of (0, linq_1.values)(state.model.schemas.objects)) {
        // did we already inline this objecct
        if (schema.language.default.inlined) {
            continue;
        }
        // we have an object, let's process it.
        createVirtualProperties(schema, new Array(), threshold, conflicts);
    }
    if ((0, linq_1.length)(conflicts) > 0) {
        // dolauli need to figure out how inline-properties is used in README.md
        state.error('You have one or more circular references in your model, you must add configuration entries to specify which models won\'t be inlined.', ['inline-properties']);
        for (const each of conflicts) {
            state.error(each, ['circular reference']);
        }
        throw new Error('Circular references exists, must mark models as `no-inline`');
    }
    //dolauli update operations under commands
    for (const operation of (0, linq_1.values)(state.model.commands.operations)) {
        createVirtualParameters(operation);
    }
    return state.model;
}
exports.createVirtuals = createVirtuals;
function shouldBeRequired(operation, virtualProperty) {
    const shouldBeOptional = operation.commandType === command_operation_1.CommandType.GetPut || operation.commandType === command_operation_1.CommandType.ManagedIdentityUpdate;
    if (!shouldBeOptional) {
        return virtualProperty.required;
    }
    if (!virtualProperty.read && (virtualProperty.create || virtualProperty.update)) {
        return virtualProperty.required;
    }
    return false;
}
async function createInlinedPropertiesPlugin(service) {
    //const session = await startSession<PwshModel>(service, {}, codeModelSchema);
    //const result = tweakModelV2(session);
    const state = await new model_state_1.ModelState(service).init();
    await service.writeFile({ filename: 'code-model-v4-create-virtual-properties-v2.yaml', content: (0, codegen_1.serialize)(await createVirtuals(state)), sourceMap: undefined, artifactType: 'code-model-v4' });
    //return processCodeModel(createVirtuals, service, 'create-virtual-properties-v2');
}
exports.createInlinedPropertiesPlugin = createInlinedPropertiesPlugin;
//# sourceMappingURL=plugin-create-inline-properties.js.map