"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const cxapi = require("@aws-cdk/cx-api");
const construct_1 = require("../core/construct");
const tokens_1 = require("../core/tokens");
const cloudformation_token_1 = require("./cloudformation-token");
const logical_id_1 = require("./logical-id");
/**
 * A root construct which represents a single CloudFormation stack.
 */
class Stack extends construct_1.Construct {
    /**
     * Creates a new stack.
     *
     * @param parent Parent of this stack, usually a Program instance.
     * @param name The name of the CloudFormation stack. Defaults to "Stack".
     * @param props Stack properties.
     */
    constructor(parent, name, props) {
        // For unit test convenience parents are optional, so bypass the type check when calling the parent.
        super(parent, name);
        /**
         * Lists all missing contextual information.
         * This is returned when the stack is synthesized under the 'missing' attribute
         * and allows tooling to obtain the context and re-synthesize.
         */
        this.missingContext = {};
        /**
         * Used to determine if this construct is a stack.
         */
        this.isStack = true;
        /**
         * Options for CloudFormation template (like version, transform, description).
         */
        this.templateOptions = {};
        this.env = this.parseEnvironment(props);
        this.logicalIds = new logical_id_1.LogicalIDs(props && props.namingScheme ? props.namingScheme : new logical_id_1.HashedAddressingScheme());
        this.name = name || 'Stack';
    }
    /**
     * Traverses the tree and looks up for the Stack root.
     * @param node A construct in the tree
     * @returns The Stack object (throws if the node is not part of a Stack-rooted tree)
     */
    static find(node) {
        let curr = node;
        while (curr != null && !isStack(curr)) {
            curr = curr.parent;
        }
        if (curr == null) {
            throw new Error(`Cannot find a Stack parent for '${node.toString()}'`);
        }
        return curr;
    }
    /**
     * Adds a metadata annotation "aws:cdk:physical-name" to the construct if physicalName
     * is non-null. This can be used later by tools and aspects to determine if resources
     * have been created with physical names.
     */
    static annotatePhysicalName(construct, physicalName) {
        if (physicalName == null) {
            return;
        }
        construct.addMetadata('aws:cdk:physical-name', physicalName);
    }
    /**
     * Looks up a resource by path.
     *
     * @returns The Resource or undefined if not found
     */
    findResource(path) {
        const r = this.findChild(path);
        if (!r) {
            return undefined;
        }
        // found an element, check if it's a resource (duck-type)
        if (!('resourceType' in r)) {
            throw new Error(`Found a stack element for ${path} but it is not a resource: ${r.toString()}`);
        }
        return r;
    }
    /**
     * Returns the CloudFormation template for this stack by traversing
     * the tree and invoking toCloudFormation() on all Entity objects.
     */
    toCloudFormation() {
        // before we begin synthesis, we shall lock this stack, so children cannot be added
        this.lock();
        try {
            const template = {
                Description: this.templateOptions.description,
                Transform: this.templateOptions.transform,
                AWSTemplateFormatVersion: this.templateOptions.templateFormatVersion,
                Metadata: this.templateOptions.metadata
            };
            const elements = stackElements(this);
            const fragments = elements.map(e => e.toCloudFormation());
            // merge in all CloudFormation fragments collected from the tree
            for (const fragment of fragments) {
                merge(template, fragment);
            }
            // resolve all tokens and remove all empties
            const ret = tokens_1.resolve(template) || {};
            this.logicalIds.assertAllRenamesApplied();
            return ret;
        }
        finally {
            // allow mutations after synthesis is finished.
            this.unlock();
        }
    }
    /**
     * @param why more information about why region is required.
     * @returns The region in which this stack is deployed. Throws if region is not defined.
     */
    requireRegion(why) {
        if (!this.env.region) {
            throw new Error(`${why ? why + '. ' : ''}Stack requires region information. It can be either supplied via the "env" property, ` +
                `via the "${cxapi.DEFAULT_REGION_CONTEXT_KEY}" context parameters or using "aws configure"`);
        }
        return this.env.region;
    }
    /**
     * Indicate that a context key was expected
     *
     * Contains instructions on how the key should be supplied.
     * @param key Key that uniquely identifies this missing context.
     * @param details The set of parameters needed to obtain the context (specific to context provider).
     */
    reportMissingContext(key, details) {
        this.missingContext[key] = details;
    }
    /**
     * Rename a generated logical identities
     */
    renameLogical(oldId, newId) {
        // tslint:disable-next-line:no-console
        if (this.children.length > 0) {
            throw new Error("All renames must be set up before adding elements to the stack");
        }
        this.logicalIds.renameLogical(oldId, newId);
    }
    /**
     * Validate stack name
     *
     * CloudFormation stack names can include dashes in addition to the regular identifier
     * character classes, and we don't allow one of the magic markers.
     */
    _validateId(name) {
        if (!Stack.VALID_STACK_NAME_REGEX.test(name)) {
            throw new Error(`Stack name must match the regular expression: ${Stack.VALID_STACK_NAME_REGEX.toString()}, got '${name}'`);
        }
    }
    /**
     * Applied defaults to environment attributes.
     */
    parseEnvironment(props) {
        // start with `env`.
        const env = (props && props.env) || {};
        // if account is not specified, attempt to read from context.
        if (!env.account) {
            env.account = this.getContext(cxapi.DEFAULT_ACCOUNT_CONTEXT_KEY);
        }
        // if region is not specified, attempt to read from context.
        if (!env.region) {
            env.region = this.getContext(cxapi.DEFAULT_REGION_CONTEXT_KEY);
        }
        return env;
    }
}
Stack.VALID_STACK_NAME_REGEX = /^[A-Za-z][A-Za-z0-9-]*$/;
exports.Stack = Stack;
function merge(template, part) {
    for (const section of Object.keys(part)) {
        const src = part[section];
        // create top-level section if it doesn't exist
        let dest = template[section];
        if (!dest) {
            template[section] = dest = src;
        }
        else {
            // add all entities from source section to destination section
            for (const id of Object.keys(src)) {
                if (id in dest) {
                    throw new Error(`section '${section}' already contains '${id}'`);
                }
                dest[id] = src[id];
            }
        }
    }
}
const LOGICAL_ID_MD = 'aws:cdk:logicalId';
/**
 * An element of a CloudFormation stack.
 */
class StackElement extends construct_1.Construct {
    /**
     * Returns `true` if a construct is a stack element (i.e. part of the
     * synthesized cloudformation template).
     *
     * Uses duck-typing instead of `instanceof` to allow stack elements from different
     * versions of this library to be included in the same stack.
     *
     * @returns The construct as a stack element or undefined if it is not a stack element.
     */
    static _asStackElement(construct) {
        if ('logicalId' in construct && 'toCloudFormation' in construct) {
            return construct;
        }
        else {
            return undefined;
        }
    }
    /**
     * Creates an entity and binds it to a tree.
     * Note that the root of the tree must be a Stack object (not just any Root).
     *
     * @param parent The parent construct
     * @param props Construct properties
     */
    constructor(parent, name) {
        super(parent, name);
        const s = Stack.find(this);
        if (!s) {
            throw new Error('The tree root must be derived from "Stack"');
        }
        this.stack = s;
        this.addMetadata(LOGICAL_ID_MD, new tokens_1.Token(() => this.logicalId), this.constructor);
        this.logicalId = this.stack.logicalIds.getLogicalId(this);
    }
    /**
     * @returns the stack trace of the point where this Resource was created from, sourced
     *          from the +metadata+ entry typed +aws:cdk:logicalId+, and with the bottom-most
     *          node +internal+ entries filtered.
     */
    get creationStackTrace() {
        return filterStackTrace(this.metadata.find(md => md.type === LOGICAL_ID_MD).trace);
        function filterStackTrace(stack) {
            const result = Array.of(...stack);
            while (result.length > 0 && shouldFilter(result[result.length - 1])) {
                result.pop();
            }
            // It's weird if we filtered everything, so return the whole stack...
            return result.length === 0 ? stack : result;
        }
        function shouldFilter(str) {
            return str.match(/[^(]+\(internal\/.*/) !== null;
        }
    }
    /**
     * Return the path with respect to the stack
     */
    get stackPath() {
        return this.ancestors(this.stack).map(c => c.id).join(construct_1.PATH_SEP);
    }
    get dependencyElements() {
        return [this];
    }
}
exports.StackElement = StackElement;
/**
 * A construct, which is part of a stack and can be referenced using it's logical ID
 * using the CloudFormation intrinsic function { Ref: ID }.
 */
class Referenceable extends StackElement {
    /**
     * Returns a token to a CloudFormation { Ref } that references this entity based on it's logical ID.
     */
    get ref() {
        return new cloudformation_token_1.CloudFormationToken({ Ref: this.logicalId }, `${this.logicalId}.Ref`);
    }
}
exports.Referenceable = Referenceable;
/**
 * Return whether the given object is a Stack.
 *
 * We do attribute detection since we can't reliably use 'instanceof'.
 */
function isStack(construct) {
    return construct.isStack;
}
/**
 * Collect all StackElements from a construct
 *
 * @param node Root node to collect all StackElements from
 * @param into Array to append StackElements to
 * @returns The same array as is being collected into
 */
function stackElements(node, into = []) {
    const element = StackElement._asStackElement(node);
    if (element) {
        into.push(element);
    }
    for (const child of node.children) {
        stackElements(child, into);
    }
    return into;
}
//# sourceMappingURL=data:application/json;base64, 