"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.CdmProjection = void 0;
const internal_1 = require("../../internal");
const ExpressionTree_1 = require("../../ResolvedModel/ExpressionParser/ExpressionTree");
const InputValues_1 = require("../../ResolvedModel/ExpressionParser/InputValues");
/**
 * Class for projection
 */
class CdmProjection extends internal_1.CdmObjectDefinitionBase {
    /**
     * Projection constructor
     */
    constructor(ctx) {
        super(ctx);
        this.TAG = CdmProjection.name;
        this.objectType = internal_1.cdmObjectType.projectionDef;
        this.operations = new internal_1.CdmOperationCollection(ctx, this);
    }
    /**
     * Property of a projection that holds the source of the operation
     */
    get source() {
        return this._source;
    }
    set source(source) {
        if (source) {
            source.owner = this;
        }
        this._source = source;
    }
    /**
     * @inheritdoc
     */
    copy(resOpt, host) {
        let copy;
        if (!host) {
            copy = new CdmProjection(this.ctx);
        }
        else {
            copy = host;
            copy.ctx = this.ctx;
            copy.operations.clear();
        }
        copy.condition = this.condition;
        copy.source = this.source ? this.source.copy() : null;
        for (const operation of this.operations) {
            copy.operations.push(operation.copy());
        }
        // Don't do anything else after this, as it may cause InDocument to become dirty
        copy.inDocument = this.inDocument;
        return copy;
    }
    /**
     * @inheritdoc
     */
    getName() {
        return 'projection';
    }
    /**
     * @inheritdoc
     */
    getObjectType() {
        return internal_1.cdmObjectType.projectionDef;
    }
    /**
     * @inheritdoc
     */
    isDerivedFrom(base, resOpt) {
        // Since projections don't support inheritance, return false
        return false;
    }
    /**
     * @inheritdoc
     */
    validate() {
        const missingFields = [];
        if (!this.source) {
            const rootOwner = this.getRootOwner();
            if (rootOwner.objectType !== internal_1.cdmObjectType.typeAttributeDef) {
                // If the projection is used in an entity attribute or an extends entity
                missingFields.push('source');
            }
        }
        else if (!this.source.explicitReference || this.source.explicitReference.objectType !== internal_1.cdmObjectType.projectionDef) {
            // If reached the inner most projection
            const rootOwner = this.getRootOwner();
            if (rootOwner.objectType == internal_1.cdmObjectType.typeAttributeDef) {
                // If the projection is used in a type attribute
                internal_1.Logger.error(this.ctx, this.TAG, this.validate.name, this.atCorpusPath, internal_1.cdmLogCode.ErrProjSourceError);
            }
        }
        if (missingFields.length > 0) {
            internal_1.Logger.error(this.ctx, this.TAG, this.validate.name, this.atCorpusPath, internal_1.cdmLogCode.ErrValdnIntegrityCheckFailure, missingFields.map((s) => `'${s}'`).join(', '));
            return false;
        }
        return true;
    }
    /**
     * @inheritdoc
     */
    visit(pathFrom, preChildren, postChildren) {
        const path = this.fetchDeclaredPath(pathFrom);
        if (preChildren && preChildren(this, path)) {
            return false;
        }
        if (this.source) {
            if (this.source.visit(path + '/source/', preChildren, postChildren)) {
                return true;
            }
        }
        let result = false;
        if (this.operations && this.operations.length > 0) {
            // since this.Operations.VisitList results is non-unique attribute context paths if there are 2 or more operations of the same type.
            // e.g. with composite keys
            // the solution is to add a unique identifier to the path by adding the operation index or opIdx
            for (let opIndex = 0; opIndex < this.operations.length; opIndex++) {
                this.operations.allItems[opIndex].index = opIndex + 1;
                if ((this.operations.allItems[opIndex]) &&
                    (this.operations.allItems[opIndex].visit(`${path}/operation/index${opIndex + 1}/`, preChildren, postChildren))) {
                    result = true;
                }
                else {
                    result = false;
                }
            }
            if (result) {
                return true;
            }
        }
        if (postChildren && postChildren(this, path)) {
            return true;
        }
        return false;
    }
    /**
     * @inheritdoc
     * @internal
     */
    fetchResolvedTraits(resOpt) {
        return this.source.fetchResolvedTraits(resOpt);
    }
    /**
     * A function to construct projection context and populate the resolved attribute set that ExtractResolvedAttributes method can then extract
     * This function is the entry point for projection resolution.
     * This function is expected to do the following 3 things:
     * - Create an condition expression tree & default if appropriate
     * - Create and initialize Projection Context
     * - Process operations
     * @internal
     */
    constructProjectionContext(projDirective, attrCtx, ras = undefined) {
        var _a, _b;
        if (!attrCtx) {
            return undefined;
        }
        let projContext;
        // Add projection to context tree
        const acpProj = {
            under: attrCtx,
            type: internal_1.cdmAttributeContextType.projection,
            name: this.fetchObjectDefinitionName(),
            regarding: projDirective.ownerRef,
            includeTraits: false
        };
        const acProj = internal_1.CdmAttributeContext.createChildUnder(projDirective.resOpt, acpProj);
        const acpSource = {
            under: acProj,
            type: internal_1.cdmAttributeContextType.source,
            name: 'source',
            regarding: undefined,
            includeTraits: false
        };
        const acSource = internal_1.CdmAttributeContext.createChildUnder(projDirective.resOpt, acpSource);
        // Initialize the projection context
        const ctx = ((_a = projDirective.owner) === null || _a === void 0 ? void 0 : _a.ctx);
        if (this.source) {
            const source = this.source.fetchObjectDefinition(projDirective.resOpt);
            if (source.objectType === internal_1.cdmObjectType.projectionDef) {
                // A Projection
                projContext = this.source.explicitReference.constructProjectionContext(projDirective, acSource, ras);
            }
            else {
                // An Entity Reference
                const acpSourceProjection = {
                    under: acSource,
                    type: internal_1.cdmAttributeContextType.entity,
                    name: (_b = this.source.namedReference) !== null && _b !== void 0 ? _b : this.source.explicitReference.getName(),
                    regarding: this.source,
                    includeTraits: false
                };
                ras = this.source.fetchResolvedAttributes(projDirective.resOpt, acpSourceProjection);
                // If polymorphic keep original source as previous state
                let polySourceSet = null;
                if (projDirective.isSourcePolymorphic) {
                    polySourceSet = internal_1.ProjectionResolutionCommonUtil.getPolymorphicSourceSet(projDirective, ctx, this.source, ras);
                }
                // Now initialize projection attribute state
                const pasSet = internal_1.ProjectionResolutionCommonUtil.initializeProjectionAttributeStateSet(projDirective, ctx, ras, projDirective.isSourcePolymorphic, polySourceSet);
                projContext = new internal_1.ProjectionContext(projDirective, ras.attributeContext);
                projContext.currentAttributeStateSet = pasSet;
            }
        }
        else {
            // A type attribute
            // Initialize projection attribute state
            const pasSet = internal_1.ProjectionResolutionCommonUtil.initializeProjectionAttributeStateSet(projDirective, ctx, ras);
            projContext = new internal_1.ProjectionContext(projDirective, ras.attributeContext);
            projContext.currentAttributeStateSet = pasSet;
        }
        const inputValues = new InputValues_1.InputValues(projDirective);
        const isConditionValid = ExpressionTree_1.ExpressionTree.evaluateCondition(this.condition, inputValues);
        if (isConditionValid && this.operations && this.operations.length > 0) {
            // Just in case new operations were added programmatically, reindex operations
            for (let i = 0; i < this.operations.length; i++) {
                this.operations.allItems[i].index = i + 1;
            }
            // Operation
            const acpGenAttrSet = {
                under: attrCtx,
                type: internal_1.cdmAttributeContextType.generatedSet,
                name: '_generatedAttributeSet'
            };
            const acGenAttrSet = internal_1.CdmAttributeContext.createChildUnder(projDirective.resOpt, acpGenAttrSet);
            // Start with an empty list for each projection
            let pasOperations = new internal_1.ProjectionAttributeStateSet(projContext.currentAttributeStateSet.ctx);
            // The attribute set that the operation will execute on
            let operationWorkingAttributeSet;
            // The attribute set containing the attributes from the source
            const sourceAttributeSet = projContext.currentAttributeStateSet;
            // Specifies if the operation is the first on the list to run
            let firstOperationToRun = true;
            for (const operation of this.operations) {
                const operationCondition = ExpressionTree_1.ExpressionTree.evaluateCondition(operation.condition, inputValues);
                if (!operationCondition) {
                    // Skip this operation if the condition does not evaluate to true
                    continue;
                }
                // If RunSequentially is not true then all the operations will receive the source input
                // Unless the operation overwrites this behavior using the SourceInput property
                const sourceInput = operation.sourceInput != null ? operation.sourceInput : !this.runSequentially;
                // If this is the first operation to run it will get the source attribute set since the operations attribute set starts empty
                if (sourceInput || firstOperationToRun) {
                    projContext.currentAttributeStateSet = sourceAttributeSet;
                    operationWorkingAttributeSet = pasOperations;
                }
                else {
                    // Needs to create a copy since this set can be modified by the operation
                    projContext.currentAttributeStateSet = pasOperations.copy();
                    operationWorkingAttributeSet = new internal_1.ProjectionAttributeStateSet(projContext.currentAttributeStateSet.ctx);
                }
                // Evaluate projections and apply to empty state
                const newPasOperations = operation.appendProjectionAttributeState(projContext, operationWorkingAttributeSet, acGenAttrSet);
                // If the operations fails or it is not implemented the projection cannot be evaluated so keep previous valid state
                if (newPasOperations !== undefined) {
                    firstOperationToRun = false;
                    pasOperations = newPasOperations;
                }
            }
            // If no operation ran successfully pasOperations will be empty
            if (!firstOperationToRun) {
                // Finally update the current state to the projection context
                projContext.currentAttributeStateSet = pasOperations;
            }
        }
        else {
            // Pass Through - no operations to process
        }
        return projContext;
    }
    /**
     * Create resolved attribute set based on the CurrentResolvedAttribute array
     * @internal
     */
    extractResolvedAttributes(projCtx, attCtxUnder) {
        const resolvedAttributeSet = new internal_1.ResolvedAttributeSet();
        resolvedAttributeSet.attributeContext = attCtxUnder;
        if (!projCtx) {
            internal_1.Logger.error(this.ctx, CdmProjection.name, this.extractResolvedAttributes.name, this.atCorpusPath, internal_1.cdmLogCode.ErrProjFailedToResolve);
            return resolvedAttributeSet;
        }
        for (const pas of projCtx.currentAttributeStateSet.states) {
            resolvedAttributeSet.merge(pas.currentResolvedAttribute);
        }
        return resolvedAttributeSet;
    }
    getRootOwner() {
        let rootOwner = this;
        do {
            rootOwner = rootOwner.owner;
            // A projection can be inside an entity reference, so take the owner again to get the projection.
            if (rootOwner && rootOwner.owner && rootOwner.owner.objectType === internal_1.cdmObjectType.projectionDef) {
                rootOwner = rootOwner.owner;
            }
        } while (rootOwner && rootOwner.objectType === internal_1.cdmObjectType.projectionDef);
        return rootOwner;
    }
}
exports.CdmProjection = CdmProjection;
//# sourceMappingURL=CdmProjection.js.map