// Copyright 2016-2020, Pulumi Corporation.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package python

import (
	"bytes"
	"fmt"
	"io"
	"os"
	"path"
	"path/filepath"
	"slices"
	"sort"
	"strings"

	"github.com/zclconf/go-cty/cty"

	"github.com/hashicorp/hcl/v2"
	"github.com/pulumi/pulumi/pkg/v3/codegen"
	"github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model"
	"github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model/format"
	"github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/syntax"
	"github.com/pulumi/pulumi/pkg/v3/codegen/pcl"
	"github.com/pulumi/pulumi/pkg/v3/codegen/schema"
	"github.com/pulumi/pulumi/sdk/v3/go/common/encoding"
	"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
	"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
)

const stackRefQualifiedName = "pulumi.StackReference"

type generator struct {
	// The formatter to use when generating code.
	*format.Formatter

	program     *pcl.Program
	diagnostics hcl.Diagnostics

	configCreated bool
	quotes        map[model.Expression]string
	isComponent   bool

	// insideTypedDict is used to track if the generator is currently inside a TypedDict so that
	// nested TypedDicts can be handled correctly.
	insideTypedDict bool
}

func GenerateProgram(program *pcl.Program) (map[string][]byte, hcl.Diagnostics, error) {
	pcl.MapProvidersAsResources(program)
	g, err := newGenerator(program)
	if err != nil {
		return nil, nil, err
	}

	// Linearize the nodes into an order appropriate for procedural code generation.
	nodes := pcl.Linearize(program)

	// Creating a list to store and later print helper methods if they turn out to be needed
	preambleHelperMethods := codegen.NewStringSet()

	var main bytes.Buffer
	g.genPreamble(&main, program, preambleHelperMethods)
	for _, n := range nodes {
		g.genNode(&main, n)
	}

	files := map[string][]byte{
		"__main__.py": main.Bytes(),
	}

	for componentDir, component := range program.CollectComponents() {
		componentFilename := strings.ReplaceAll(filepath.Base(componentDir), "-", "_")
		componentName := component.DeclarationName()
		componentGenerator, err := newGenerator(component.Program)
		if err != nil {
			return files, componentGenerator.diagnostics, err
		}

		// mark the generator to target components
		componentGenerator.isComponent = true

		componentPreambleMethods := codegen.NewStringSet()
		var componentBuffer bytes.Buffer
		// generate imports for the component
		componentGenerator.genPreamble(&componentBuffer, component.Program, componentPreambleMethods)
		componentGenerator.genComponentDefinition(&componentBuffer, component, componentName)
		files[componentFilename+".py"] = componentBuffer.Bytes()
	}
	return files, g.diagnostics, nil
}

func componentInputElementType(pclType model.Type) string {
	switch pclType {
	case model.BoolType:
		return "bool"
	case model.IntType:
		return "int"
	case model.NumberType:
		return "float"
	case model.StringType:
		return "str"
	default:
		switch pclType := pclType.(type) {
		case *model.ListType:
			elementType := componentInputElementType(pclType.ElementType)
			return fmt.Sprintf("list[%s]", elementType)
		case *model.MapType:
			elementType := componentInputElementType(pclType.ElementType)
			return fmt.Sprintf("Dict[str, %s]", elementType)
		// reduce option(T) to just T
		// the TypedDict has total=False which means all properties are optional by default
		case *model.UnionType:
			if len(pclType.ElementTypes) == 2 && pclType.ElementTypes[0] == model.NoneType {
				return componentInputElementType(pclType.ElementTypes[1])
			} else if len(pclType.ElementTypes) == 2 && pclType.ElementTypes[1] == model.NoneType {
				return componentInputElementType(pclType.ElementTypes[0])
			} else {
				return "Any"
			}
		default:
			return "Any"
		}
	}
}

// collectObjectTypedConfigVariables returns the object types in config variables need to be emitted
// as classes.
func collectObjectTypedConfigVariables(component *pcl.Component) map[string]*model.ObjectType {
	objectTypes := map[string]*model.ObjectType{}
	for _, config := range component.Program.ConfigVariables() {
		switch configType := config.Type().(type) {
		case *model.ObjectType:
			objectTypes[config.Name()] = configType
		case *model.ListType:
			switch elementType := configType.ElementType.(type) {
			case *model.ObjectType:
				objectTypes[config.Name()] = elementType
			}
		case *model.MapType:
			switch elementType := configType.ElementType.(type) {
			case *model.ObjectType:
				objectTypes[config.Name()] = elementType
			}
		}
	}

	return objectTypes
}

func (g *generator) genComponentDefinition(w io.Writer, component *pcl.Component, componentName string) {
	configVars := component.Program.ConfigVariables()
	hasAnyInputVariables := len(configVars) > 0
	if hasAnyInputVariables {
		objectTypedConfigs := collectObjectTypedConfigVariables(component)
		variableNames := pcl.SortedStringKeys(objectTypedConfigs)
		// generate resource args for this component
		for _, variableName := range variableNames {
			objectType := objectTypedConfigs[variableName]
			objectTypeName := title(variableName)
			g.Fprintf(w, "class %s(TypedDict, total=False):\n", objectTypeName)
			g.Indented(func() {
				propertyNames := pcl.SortedStringKeys(objectType.Properties)
				for _, propertyName := range propertyNames {
					propertyType := objectType.Properties[propertyName]
					inputType := componentInputElementType(propertyType)
					g.Fprintf(w, "%s%s: Input[%s]\n",
						g.Indent,
						propertyName,
						inputType)
				}
			})
			g.Fgen(w, "\n")
		}

		// emit args class
		g.Fgenf(w, "class %sArgs(TypedDict, total=False):\n", componentName)
		g.Indented(func() {
			// define constructor args
			for _, configVar := range configVars {
				argName := configVar.Name()
				argType := componentInputElementType(configVar.Type())
				switch configType := configVar.Type().(type) {
				case *model.ObjectType:
					// for objects of type T, generate T as is
					argType = title(configVar.Name())
				case *model.ListType:
					// for list(T) where T is an object type, generate List[T]
					switch configType.ElementType.(type) {
					case *model.ObjectType:
						objectTypeName := title(configVar.Name())
						argType = fmt.Sprintf("list(%s)", objectTypeName)
					}
				case *model.MapType:
					// for map(T) where T is an object type, generate Dict[str, T]
					switch configType.ElementType.(type) {
					case *model.ObjectType:
						objectTypeName := title(configVar.Name())
						argType = fmt.Sprintf("Dict[str, %s]", objectTypeName)
					}
				}

				argType = fmt.Sprintf("Input[%s]", argType)
				g.Fgenf(w, "%s%s: %s", g.Indent, argName, argType)
				g.Fgen(w, "\n")
			}
		})

		g.Fgen(w, "\n")
	}

	componentToken := "components:index:" + componentName
	g.Fgenf(w, "class %s(pulumi.ComponentResource):\n", componentName)
	g.Indented(func() {
		if hasAnyInputVariables {
			g.Fgenf(w, "%sdef __init__(self, name: str, args: %s, opts:Optional[pulumi.ResourceOptions] = None):\n",
				g.Indent,
				componentName+"Args")

			g.Fgenf(w, "%s%ssuper().__init__(\"%s\", name, args, opts)\n",
				g.Indent,
				g.Indent,
				componentToken)
		} else {
			g.Fgenf(w, "%sdef __init__(self, name: str, opts: Optional[pulumi.ResourceOptions] = None):\n", g.Indent)
			g.Fgenf(w, "%s%ssuper().__init__(\"%s\", name, {}, opts)\n",
				g.Indent,
				g.Indent,
				componentToken)
		}

		g.Fgen(w, "\n")

		g.Indented(func() {
			for _, node := range pcl.Linearize(component.Program) {
				switch node := node.(type) {
				case *pcl.LocalVariable:
					g.genLocalVariable(w, node)
					g.Fgen(w, "\n")
				case *pcl.Component:
					// set options { parent = self } for the component resource
					// where "self" is a reference to the component resource itself
					if node.Options == nil {
						node.Options = &pcl.ResourceOptions{}
					}

					if node.Options.Parent == nil {
						node.Options.Parent = model.ConstantReference(&model.Constant{
							Name: "self",
						})
					}
					g.genComponent(w, node)
					g.Fgen(w, "\n")
				case *pcl.Resource:
					// set options { parent = self } for the component resource
					// where "self" is a reference to the component resource itself
					if node.Options == nil {
						node.Options = &pcl.ResourceOptions{}
					}

					if node.Options.Parent == nil {
						node.Options.Parent = model.ConstantReference(&model.Constant{
							Name: "self",
						})
					}
					g.genResource(w, node)
					g.Fgen(w, "\n")
				}
			}

			outputVars := component.Program.OutputVariables()
			for _, output := range outputVars {
				g.Fgenf(w, "%sself.%s = %v\n", g.Indent, output.Name(), output.Value)
			}

			if len(outputVars) == 0 {
				g.Fgenf(w, "%sself.register_outputs()\n", g.Indent)
			} else {
				g.Fgenf(w, "%sself.register_outputs({\n", g.Indent)
				g.Indented(func() {
					for index, output := range outputVars {
						g.Fgenf(w, "%s'%s': %v", g.Indent, output.Name(), output.Value)
						if index != len(outputVars)-1 {
							g.Fgen(w, ", ")
						}
						g.Fgen(w, "\n")
					}
				})
				g.Fgenf(w, "%s})", g.Indent)
			}
		})
	})
}

func GenerateProject(
	directory string, project workspace.Project,
	program *pcl.Program, localDependencies map[string]string,
) error {
	files, diagnostics, err := GenerateProgram(program)
	if err != nil {
		return err
	}
	if diagnostics.HasErrors() {
		return diagnostics
	}

	// Check the project for "main" as that changes where we write out files and some relative paths.
	rootDirectory := directory
	if project.Main != "" {
		directory = filepath.Join(rootDirectory, project.Main)
		// mkdir -p the subdirectory
		err = os.MkdirAll(directory, 0o700)
		if err != nil {
			return fmt.Errorf("create main directory: %w", err)
		}
	}

	var options map[string]interface{}
	if _, ok := localDependencies["pulumi"]; ok {
		options = map[string]interface{}{
			"virtualenv": "venv",
		}
	}

	// Set the runtime to "python" then marshal to Pulumi.yaml
	project.Runtime = workspace.NewProjectRuntimeInfo("python", options)
	projectBytes, err := encoding.YAML.Marshal(project)
	if err != nil {
		return err
	}

	// Build a requirements.txt based on the packages used by program
	requirementsTxtLines := []string{}
	if path, ok := localDependencies["pulumi"]; ok {
		requirementsTxtLines = append(requirementsTxtLines, path)
	} else {
		requirementsTxtLines = append(requirementsTxtLines, "pulumi>=3.0.0,<4.0.0")
	}

	// For each package add a PackageReference line
	// find references from the main/entry program and programs of components
	packages, err := program.CollectNestedPackageSnapshots()
	if err != nil {
		return err
	}

	for _, p := range packages {
		if p.Name == "pulumi" {
			continue
		}
		if path, ok := localDependencies[p.Name]; ok {
			requirementsTxtLines = append(requirementsTxtLines, path)
		} else {
			if err := p.ImportLanguages(map[string]schema.Language{"python": Importer}); err != nil {
				return err
			}
			packageName := "pulumi-" + p.Name
			if langInfo, found := p.Language["python"]; found {
				pyInfo, ok := langInfo.(PackageInfo)
				if ok && pyInfo.PackageName != "" {
					packageName = pyInfo.PackageName
				}
			}
			if p.Version != nil {
				requirementsTxtLines = append(requirementsTxtLines, fmt.Sprintf("%s==%s", packageName, p.Version.String()))
			} else {
				requirementsTxtLines = append(requirementsTxtLines, packageName)
			}
		}
	}

	// We want the requirements.txt files we generate to be stable, so we sort the
	// lines before obtaining the bytes.
	slices.Sort(requirementsTxtLines)
	files["requirements.txt"] = []byte(strings.Join(requirementsTxtLines, "\n") + "\n")

	// Add the language specific .gitignore
	files[".gitignore"] = []byte(`*.pyc
venv/`)

	for filename, data := range files {
		outPath := path.Join(directory, filename)
		err := os.WriteFile(outPath, data, 0o600)
		if err != nil {
			return fmt.Errorf("could not write output program: %w", err)
		}
	}

	// Write out the Pulumi.yaml
	err = os.WriteFile(path.Join(rootDirectory, "Pulumi.yaml"), projectBytes, 0o600)
	if err != nil {
		return fmt.Errorf("write Pulumi.yaml: %w", err)
	}

	return nil
}

func newGenerator(program *pcl.Program) (*generator, error) {
	// Import Python-specific schema info.
	packages, err := program.PackageSnapshots()
	if err != nil {
		return nil, err
	}
	for _, p := range packages {
		if err := p.ImportLanguages(map[string]schema.Language{"python": Importer}); err != nil {
			return nil, err
		}
	}

	g := &generator{
		program: program,
		quotes:  map[model.Expression]string{},
	}
	g.Formatter = format.NewFormatter(g)

	return g, nil
}

// genLeadingTrivia generates the list of leading trivia associated with a given token.
func (g *generator) genLeadingTrivia(w io.Writer, token syntax.Token) {
	// TODO(pdg): whitespace
	for _, t := range token.LeadingTrivia {
		if c, ok := t.(syntax.Comment); ok {
			g.genComment(w, c)
		}
	}
}

// genTrailingTrivia generates the list of trailing trivia associated with a given token.
func (g *generator) genTrailingTrivia(w io.Writer, token syntax.Token) {
	// TODO(pdg): whitespace
	for _, t := range token.TrailingTrivia {
		if c, ok := t.(syntax.Comment); ok {
			g.genComment(w, c)
		}
	}
}

// genTrivia generates the list of trivia associated with a given token.
func (g *generator) genTrivia(w io.Writer, token syntax.Token) {
	g.genLeadingTrivia(w, token)
	g.genTrailingTrivia(w, token)
}

// genComment generates a comment into the output.
func (g *generator) genComment(w io.Writer, comment syntax.Comment) {
	for _, l := range comment.Lines {
		g.Fgenf(w, "%s#%s\n", g.Indent, l)
	}
}

// rewriteApplyLambdaBody rewrites the body of a lambda where it rewrites the usage of lambda variables
// into an index expression of a dictionary. for example lambda arg `value` will become <argsParamName>["value"]
func rewriteApplyLambdaBody(applyLambda *model.AnonymousFunctionExpression, argsParamName string) model.Expression {
	rewriter := func(expr model.Expression) (model.Expression, hcl.Diagnostics) {
		switch expr := expr.(type) {
		case *model.ScopeTraversalExpression:
			if len(expr.Parts) == 1 {
				// check whether this expression is traversing a lambda arg
				// rewrite arg into argsParamName["argName"]
				for _, param := range applyLambda.Signature.Parameters {
					if param.Name == expr.RootName {
						return &model.IndexExpression{
							Collection: model.VariableReference(&model.Variable{
								Name: argsParamName,
							}),
							Key: &model.LiteralValueExpression{
								Value: cty.StringVal(fmt.Sprintf("'%s'", param.Name)),
							},
						}, nil
					}
				}
			}
		}

		return expr, nil
	}

	rewrittenBody, _ := model.VisitExpression(applyLambda.Body, model.IdentityVisitor, rewriter)

	return rewrittenBody
}

func (g *generator) genPreamble(w io.Writer, program *pcl.Program, preambleHelperMethods codegen.StringSet) {
	// Print the pulumi import at the top.
	g.Fprintln(w, "import pulumi")

	// Accumulate other imports for the various providers. Don't emit them yet, as we need to sort them later on.
	type Import struct {
		// Use an "import ${KEY} as ${.Pkg}"
		ImportAs bool
		// Only relevant for when ImportAs=true
		Pkg string
	}
	importSet := map[string]Import{}
	for _, n := range program.Nodes {
		if r, isResource := n.(*pcl.Resource); isResource {
			pcl.FixupPulumiPackageTokens(r)
			pkg, _, _, _ := r.DecomposeToken()
			if pkg == "pulumi" {
				continue
			}
			packageName := "pulumi_" + makeValidIdentifier(pkg)
			if r.Schema != nil && r.Schema.PackageReference != nil {
				pkg, err := r.Schema.PackageReference.Definition()
				if err == nil {
					if pkgInfo, ok := pkg.Language["python"].(PackageInfo); ok && pkgInfo.PackageName != "" {
						packageName = pkgInfo.PackageName
					}
				}
			}
			importSet[packageName] = Import{ImportAs: true, Pkg: makeValidIdentifier(pkg)}
		}
		diags := n.VisitExpressions(nil, func(n model.Expression) (model.Expression, hcl.Diagnostics) {
			if call, ok := n.(*model.FunctionCallExpression); ok {
				if i := g.getFunctionImports(call); len(i) > 0 && i[0] != "" {
					for _, importPackage := range i {
						importAs := strings.HasPrefix(importPackage, "pulumi_")
						var maybePkg string
						if importAs {
							maybePkg = importPackage[len("pulumi_"):]
						}
						importSet[importPackage] = Import{
							ImportAs: importAs,
							Pkg:      maybePkg,
						}
					}
				}
				if helperMethodBody, ok := getHelperMethodIfNeeded(call.Name, g.Indent); ok {
					preambleHelperMethods.Add(helperMethodBody)
				}
			}
			return n, nil
		})
		contract.Assertf(len(diags) == 0, "unexpected diagnostics reported: %v", diags)
	}

	var imports []string
	importSetNames := codegen.NewStringSet()
	for k := range importSet {
		importSetNames.Add(k)
	}
	for _, pkg := range importSetNames.SortedValues() {
		if pkg == "pulumi" {
			continue
		}
		control := importSet[pkg]
		if control.ImportAs {
			imports = append(imports, fmt.Sprintf("import %s as %s", pkg, EnsureKeywordSafe(control.Pkg)))
		} else {
			imports = append(imports, "import "+pkg)
		}
	}

	if g.isComponent {
		// add typing information
		imports = append(imports, "from typing import Optional, Dict, TypedDict, Any")
		imports = append(imports, "from pulumi import Input")
	}

	seenComponentImports := map[string]bool{}
	for _, node := range program.Nodes {
		if component, ok := node.(*pcl.Component); ok {
			componentPath := strings.ReplaceAll(filepath.Base(component.DirPath()), "-", "_")
			componentName := component.DeclarationName()
			pathAndName := componentPath + "-" + componentName
			if _, ok := seenComponentImports[pathAndName]; !ok {
				imports = append(imports, fmt.Sprintf("from %s import %s", componentPath, componentName))
				seenComponentImports[pathAndName] = true
			}
		}
	}

	// Now sort the imports and emit them.
	sort.Strings(imports)
	for _, i := range imports {
		g.Fprintln(w, i)
	}
	g.Fprint(w, "\n")

	// If we collected any helper methods that should be added, write them just before the main func
	for _, preambleHelperMethodBody := range preambleHelperMethods.SortedValues() {
		g.Fprintf(w, "%s\n\n", preambleHelperMethodBody)
	}
}

func (g *generator) genNode(w io.Writer, n pcl.Node) {
	switch n := n.(type) {
	case *pcl.Resource:
		g.genResource(w, n)
	case *pcl.ConfigVariable:
		g.genConfigVariable(w, n)
	case *pcl.LocalVariable:
		g.genLocalVariable(w, n)
	case *pcl.OutputVariable:
		g.genOutputVariable(w, n)
	case *pcl.Component:
		g.genComponent(w, n)
	}
}

func tokenToQualifiedName(pkg, module, member string) string {
	components := strings.Split(module, "/")
	for i, component := range components {
		components[i] = PyName(component)
	}
	module = strings.Join(components, ".")
	if module != "" {
		module = "." + module
	}

	return fmt.Sprintf("%s%s.%s", PyName(pkg), module, title(member))
}

// resourceTypeName computes the qualified name of a python resource.
func resourceTypeName(r *pcl.Resource) (string, hcl.Diagnostics) {
	// Compute the resource type from the Pulumi type token.
	pkg, module, member, diagnostics := r.DecomposeToken()
	pcl.FixupPulumiPackageTokens(r)

	// Normalize module.
	if r.Schema != nil {
		pkg, err := r.Schema.PackageReference.Definition()
		if err != nil {
			diagnostics = append(diagnostics, &hcl.Diagnostic{
				Severity: hcl.DiagError,
				Summary:  "unable to bind schema for resource",
				Detail:   err.Error(),
				Subject:  r.Definition.Syntax.DefRange().Ptr(),
			})
		} else {
			err = pkg.ImportLanguages(map[string]schema.Language{"python": Importer})
			contract.AssertNoErrorf(err, "failed to import python language plugin for package %s", pkg.Name)
			if lang, ok := pkg.Language["python"]; ok {
				if pkgInfo, ok := lang.(PackageInfo); ok {
					if m, ok := pkgInfo.ModuleNameOverrides[module]; ok {
						module = m
					}
				}
			}
		}
	}

	return tokenToQualifiedName(pkg, module, member), diagnostics
}

func (g *generator) typedDictEnabled(expr model.Expression, typ model.Type) bool {
	schemaType, ok := pcl.GetSchemaForType(typ)
	if !ok {
		return false
	}

	schemaType = codegen.UnwrapType(schemaType)

	objType, ok := schemaType.(*schema.ObjectType)
	if !ok {
		return false
	}

	pkg, err := objType.PackageReference.Definition()
	contract.AssertNoErrorf(err, "error loading definition for package %q", objType.PackageReference.Name())
	if lang, ok := pkg.Language["python"]; ok {
		if pkgInfo, ok := lang.(PackageInfo); ok {
			if typedDictEnabled(pkgInfo.InputTypes) {
				return true
			}
		}
	}

	return false
}

// argumentTypeName computes the Python argument class name for the given expression and model type.
func (g *generator) argumentTypeName(expr model.Expression, destType model.Type) string {
	schemaType, ok := pcl.GetSchemaForType(destType)
	if !ok {
		return ""
	}

	schemaType = codegen.UnwrapType(schemaType)

	objType, ok := schemaType.(*schema.ObjectType)
	if !ok {
		return ""
	}

	token := objType.Token
	tokenRange := expr.SyntaxNode().Range()

	// Example: aws, s3/BucketLogging, BucketLogging, []Diagnostics
	pkgName, module, member, diagnostics := pcl.DecomposeToken(token, tokenRange)
	contract.Assertf(len(diagnostics) == 0, "unexpected diagnostics reported: %v", diagnostics)

	modName := objType.PackageReference.TokenToModule(token)

	// Normalize module.
	pkg, err := objType.PackageReference.Definition()
	contract.AssertNoErrorf(err, "error loading definition for package %q", objType.PackageReference.Name())
	if lang, ok := pkg.Language["python"]; ok {
		if pkgInfo, ok := lang.(PackageInfo); ok {
			if m, ok := pkgInfo.ModuleNameOverrides[module]; ok {
				modName = m
			}
		}
	}
	return tokenToQualifiedName(pkgName, modName, member) + "Args"
}

// makeResourceName returns the expression that should be emitted for a resource's "name" parameter given its base name
// and the count variable name, if any.
func (g *generator) makeResourceName(baseName, count string) string {
	if count == "" {
		if g.isComponent {
			return fmt.Sprintf(`f"{name}-%s"`, baseName)
		}
		return fmt.Sprintf(`"%s"`, baseName)
	}

	if g.isComponent {
		return fmt.Sprintf(`f"{name}-%s-{%s}"`, baseName, count)
	}
	return fmt.Sprintf(`f"%s-{%s}"`, baseName, count)
}

func (g *generator) lowerResourceOptions(opts *pcl.ResourceOptions) (*model.Block, []*quoteTemp) {
	if opts == nil {
		return nil, nil
	}

	var block *model.Block
	var temps []*quoteTemp
	appendOption := func(name string, value model.Expression) {
		if block == nil {
			block = &model.Block{
				Type: "options",
				Body: &model.Body{},
			}
		}

		value, valueTemps := g.lowerExpression(value, value.Type())
		temps = append(temps, valueTemps...)

		block.Body.Items = append(block.Body.Items, &model.Attribute{
			Tokens: syntax.NewAttributeTokens(name),
			Name:   name,
			Value:  value,
		})
	}

	if opts.Parent != nil {
		appendOption("parent", opts.Parent)
	}
	if opts.Provider != nil {
		appendOption("provider", opts.Provider)
	}
	if opts.DependsOn != nil {
		appendOption("depends_on", opts.DependsOn)
	}
	if opts.Protect != nil {
		appendOption("protect", opts.Protect)
	}
	if opts.RetainOnDelete != nil {
		appendOption("retain_on_delete", opts.RetainOnDelete)
	}
	if opts.IgnoreChanges != nil {
		appendOption("ignore_changes", opts.IgnoreChanges)
	}
	if opts.DeletedWith != nil {
		appendOption("deleted_with", opts.DeletedWith)
	}

	return block, temps
}

func (g *generator) genResourceOptions(w io.Writer, block *model.Block, hasInputs bool) {
	if block == nil {
		return
	}

	prefix := " "
	if hasInputs {
		prefix = "\n" + g.Indent
	}
	g.Fprintf(w, ",%sopts = pulumi.ResourceOptions(", prefix)
	g.Indented(func() {
		for i, item := range block.Body.Items {
			if i > 0 {
				g.Fprintf(w, ",\n%s", g.Indent)
			}
			attr := item.(*model.Attribute)
			g.Fgenf(w, "%s=%v", attr.Name, attr.Value)
		}
	})
	g.Fprint(w, ")")
}

// genResourceDeclaration handles the generation of instantiations resources.
func (g *generator) genResourceDeclaration(w io.Writer, r *pcl.Resource, needsDefinition bool) {
	qualifiedMemberName, diagnostics := resourceTypeName(r)
	g.diagnostics = append(g.diagnostics, diagnostics...)
	optionsBag, temps := g.lowerResourceOptions(r.Options)
	name := r.LogicalName()
	nameVar := PyName(r.Name())

	if needsDefinition {
		g.genTrivia(w, r.Definition.Tokens.GetType(""))
		for _, l := range r.Definition.Tokens.Labels {
			g.genTrivia(w, l)
		}
		g.genTrivia(w, r.Definition.Tokens.GetOpenBrace())
	}

	if r.Schema != nil {
		for _, input := range r.Inputs {
			destType, diagnostics := r.InputType.Traverse(hcl.TraverseAttr{Name: input.Name})
			g.diagnostics = append(g.diagnostics, diagnostics...)
			value, valueTemps := g.lowerExpression(input.Value, destType.(model.Type))
			temps = append(temps, valueTemps...)
			input.Value = value
		}
	}

	g.genTemps(w, temps)

	instantiate := func(resName string) {
		g.Fgenf(w, "%s(%s", qualifiedMemberName, resName)
		indenter := func(f func()) { f() }
		if len(r.Inputs) > 1 {
			indenter = g.Indented
		}
		indenter(func() {
			for _, attr := range r.Inputs {
				propertyName := InitParamName(attr.Name)
				// special case: pulumi.StackReference requires `stack_name` instead of `name`
				if qualifiedMemberName == stackRefQualifiedName && propertyName == "name" {
					propertyName = "stack_name"
				}
				if len(r.Inputs) == 1 {
					g.Fgenf(w, ", %s=%.v", propertyName, attr.Value)
				} else {
					g.Fgenf(w, ",\n%s%s=%.v", g.Indent, propertyName, attr.Value)
				}
			}
			g.genResourceOptions(w, optionsBag, len(r.Inputs) != 0)
		})
		g.Fprint(w, ")")
	}

	if r.Options != nil && r.Options.Range != nil {
		rangeExpr := r.Options.Range
		rangeType := r.Options.Range.Type()

		if model.ContainsOutputs(rangeType) {
			loweredRangeExpr, rangeExprTemps := g.lowerExpression(rangeExpr, rangeType)
			if model.InputType(model.BoolType).ConversionFrom(r.Options.Range.Type()) == model.SafeConversion {
				g.Fgenf(w, "%s%s = None\n", g.Indent, nameVar)
			} else {
				g.Fgenf(w, "%s%s = []\n", g.Indent, nameVar)
			}
			localFuncName := "create_" + PyName(r.LogicalName())

			// Generate a local definition which actually creates the resources
			g.Fgenf(w, "def %s(range_body):\n", localFuncName)
			g.Indented(func() {
				r.Options.Range = model.VariableReference(&model.Variable{
					Name:         "range_body",
					VariableType: model.ResolveOutputs(rangeExpr.Type()),
				})
				g.genResourceDeclaration(w, r, false)
				g.Fgen(w, "\n")
			})

			g.genTemps(w, rangeExprTemps)

			switch expr := loweredRangeExpr.(type) {
			case *model.FunctionCallExpression:
				if expr.Name == pcl.IntrinsicApply {
					applyArgs, applyLambda := pcl.ParseApplyCall(expr)

					// Step 1: generate the apply function call:
					if len(applyArgs) == 1 {
						// If we only have a single output, just generate a normal `.apply`
						g.Fgenf(w, "%v.apply(", applyArgs[0])
					} else {
						// Otherwise, generate a call to `pulumi.Output.all([]).apply()`.
						g.Fgen(w, "pulumi.Output.all(\n")
						g.Indented(func() {
							for i, arg := range applyArgs {
								argName := applyLambda.Signature.Parameters[i].Name
								g.Fgenf(w, "%s%s=%v", g.Indent, argName, arg)
								if i < len(applyArgs)-1 {
									g.Fgen(w, ",")
								}
								g.Fgen(w, "\n")
							}
						})
						g.Fgen(w, ").apply(")
					}

					// Step 2: apply lambda function arguments
					g.Fgen(w, "lambda resolved_outputs:")
					// Step 3: The function body is where the resources are generated:
					// The function body is also a non-output value so we rewrite the range of
					// the resource declaration to this non-output value
					rewrittenLambdaBody := rewriteApplyLambdaBody(applyLambda, "resolved_outputs")
					g.Fgenf(w, " %s(%.v))\n", localFuncName, rewrittenLambdaBody)
					return
				}

				// If we have anything else that returns output, just generate a normal `.apply`
				g.Fgenf(w, "%.20v.apply(%s)\n", loweredRangeExpr, localFuncName)
				return
			case *model.ForExpression:
				// A list generator that contains outputs looks like list(output(T))
				// when we pass that list into `Output.all` it returns a list with a single element,
				// that element is another list of all resolved items
				// that is why we index the resolved outputs at 0
				g.Fgenf(w, "pulumi.Output.all(%v).apply(lambda resolved_outputs: %s(resolved_outputs[0]))\n",
					rangeExpr,
					localFuncName)
				return
			case *model.TupleConsExpression:
				// A list that contains outputs looks like list(output(T))
				// ideally we want this to be output(list(T)) and then call apply:
				// so we call pulumi.all to lift the elements of the list, then call apply
				g.Fgen(w, "pulumi.Output.all(\n")
				g.Indented(func() {
					for i, item := range expr.Expressions {
						g.Fgenf(w, "%s%v", g.Indent, item)
						if i < len(expr.Expressions)-1 {
							g.Fgenf(w, ",")
						}

						g.Fgen(w, "\n")
					}
				})

				g.Fgenf(w, ").apply(%s)\n", localFuncName)
				return

			default:
				// If we have anything else that returns output, just generate a normal `.apply`
				g.Fgenf(w, "%v.apply(%s)\n", rangeExpr, localFuncName)
				return
			}
		}

		if model.InputType(model.BoolType).ConversionFrom(r.Options.Range.Type()) == model.SafeConversion {
			if needsDefinition {
				g.Fgenf(w, "%s%s = None\n", g.Indent, nameVar)
			}

			g.Fgenf(w, "%sif %.v:\n", g.Indent, rangeExpr)
			g.Indented(func() {
				g.Fprintf(w, "%s%s = ", g.Indent, nameVar)
				instantiate(g.makeResourceName(name, ""))
				g.Fprint(w, "\n")
			})
		} else {
			if needsDefinition {
				g.Fgenf(w, "%s%s = []\n", g.Indent, nameVar)
			}

			resKey := "key"
			if model.InputType(model.NumberType).ConversionFrom(rangeExpr.Type()) != model.NoConversion {
				g.Fgenf(w, "%sfor range in [{\"value\": i} for i in range(0, %.v)]:\n", g.Indent, rangeExpr)
				resKey = "value"
			} else {
				g.Fgenf(w, "%sfor range in [{\"key\": k, \"value\": v} for [k, v] in enumerate(%.v)]:\n", g.Indent, rangeExpr)
			}

			resName := g.makeResourceName(name, fmt.Sprintf("range['%s']", resKey))
			g.Indented(func() {
				g.Fgenf(w, "%s%s.append(", g.Indent, nameVar)
				instantiate(resName)
				g.Fprint(w, ")\n")
			})
		}
	} else {
		g.Fgenf(w, "%s%s = ", g.Indent, nameVar)
		instantiate(g.makeResourceName(name, ""))
		g.Fprint(w, "\n")
	}

	g.genTrivia(w, r.Definition.Tokens.GetCloseBrace())
}

// genResource handles the generation of instantiations of resources.
func (g *generator) genResource(w io.Writer, r *pcl.Resource) {
	g.genResourceDeclaration(w, r, true)
}

// genComponent handles the generation of instantiations of non-builtin resources.
func (g *generator) genComponent(w io.Writer, r *pcl.Component) {
	componentName := r.DeclarationName()
	optionsBag, temps := g.lowerResourceOptions(r.Options)
	name := r.LogicalName()
	nameVar := PyName(r.Name())

	g.genTrivia(w, r.Definition.Tokens.GetType(""))
	for _, l := range r.Definition.Tokens.Labels {
		g.genTrivia(w, l)
	}
	g.genTrivia(w, r.Definition.Tokens.GetOpenBrace())

	for _, input := range r.Inputs {
		value, valueTemps := g.lowerExpression(input.Value, input.Value.Type())
		temps = append(temps, valueTemps...)
		input.Value = value
	}
	g.genTemps(w, temps)

	hasInputVariables := len(r.Program.ConfigVariables()) > 0
	instantiate := func(resName string) {
		if hasInputVariables {
			g.Fgenf(w, "%s(%s, {\n", componentName, resName)
		} else {
			g.Fgenf(w, "%s(%s", componentName, resName)
		}

		indenter := func(f func()) { f() }
		if len(r.Inputs) > 1 {
			indenter = g.Indented
		}
		indenter(func() {
			for index, attr := range r.Inputs {
				propertyName := attr.Name
				if len(r.Inputs) == 1 {
					g.Fgenf(w, "'%s': %.v", propertyName, attr.Value)
				} else {
					g.Fgenf(w, "%s'%s': %.v", g.Indent, propertyName, attr.Value)
				}

				if index != len(r.Inputs)-1 {
					// add comma after each input when that property is not the last
					g.Fgen(w, ", ")
					if len(r.Inputs) > 1 {
						g.Fgen(w, "\n")
					}
				}
			}
			g.genResourceOptions(w, optionsBag, len(r.Inputs) != 0)
		})

		if hasInputVariables {
			g.Fgenf(w, "%s})", g.Indent)
		} else {
			g.Fgen(w, ")")
		}
	}

	if r.Options != nil && r.Options.Range != nil {
		rangeExpr := r.Options.Range
		if model.InputType(model.BoolType).ConversionFrom(r.Options.Range.Type()) == model.SafeConversion {
			g.Fgenf(w, "%s%s = None\n", g.Indent, nameVar)
			g.Fgenf(w, "%sif %.v:\n", g.Indent, rangeExpr)
			g.Indented(func() {
				g.Fprintf(w, "%s%s = ", g.Indent, nameVar)
				instantiate(g.makeResourceName(name, ""))
				g.Fprint(w, "\n")
			})
		} else {
			g.Fgenf(w, "%s%s = []\n", g.Indent, nameVar)

			resKey := "key"
			if model.InputType(model.NumberType).ConversionFrom(rangeExpr.Type()) != model.NoConversion {
				g.Fgenf(w, "%sfor range in [{\"value\": i} for i in range(0, %.v)]:\n", g.Indent, rangeExpr)
				resKey = "value"
			} else {
				g.Fgenf(w, "%sfor range in [{\"key\": k, \"value\": v} for [k, v] in enumerate(%.v)]:\n", g.Indent, rangeExpr)
			}

			resName := g.makeResourceName(name, fmt.Sprintf("range['%s']", resKey))
			g.Indented(func() {
				g.Fgenf(w, "%s%s.append(", g.Indent, nameVar)
				instantiate(resName)
				g.Fprint(w, ")\n")
			})
		}
	} else {
		g.Fgenf(w, "%s%s = ", g.Indent, nameVar)
		instantiate(g.makeResourceName(name, ""))
		g.Fprint(w, "\n")
	}

	g.genTrivia(w, r.Definition.Tokens.GetCloseBrace())
}

func (g *generator) genTemps(w io.Writer, temps []*quoteTemp) {
	for _, t := range temps {
		// TODO(pdg): trivia
		g.Fgenf(w, "%s%s = %.v\n", g.Indent, t.Name, t.Value)
	}
}

func (g *generator) genConfigVariable(w io.Writer, v *pcl.ConfigVariable) {
	// TODO(pdg): trivia

	if !g.configCreated {
		g.Fprintf(w, "%sconfig = pulumi.Config()\n", g.Indent)
		g.configCreated = true
	}

	getType := "_object"
	switch v.Type() {
	case model.StringType:
		getType = ""
	case model.NumberType:
		getType = "_float"
	case model.IntType:
		getType = "_int"
	case model.BoolType:
		getType = "_bool"
	}

	getOrRequire := "get"
	if v.DefaultValue == nil {
		getOrRequire = "require"
	}

	var defaultValue model.Expression
	var temps []*quoteTemp
	if v.DefaultValue != nil {
		defaultValue, temps = g.lowerExpression(v.DefaultValue, v.DefaultValue.Type())
	}
	g.genTemps(w, temps)

	if v.Description != "" {
		for _, line := range strings.Split(v.Description, "\n") {
			g.Fgenf(w, "%s# %s\n", g.Indent, line)
		}
	}
	name := PyName(v.Name())
	g.Fgenf(w, "%s%s = config.%s%s(\"%s\")\n", g.Indent, name, getOrRequire, getType, v.LogicalName())
	if defaultValue != nil {
		g.Fgenf(w, "%sif %s is None:\n", g.Indent, name)
		g.Indented(func() {
			g.Fgenf(w, "%s%s = %.v\n", g.Indent, name, defaultValue)
		})
	}
}

func (g *generator) genLocalVariable(w io.Writer, v *pcl.LocalVariable) {
	value, temps := g.lowerExpression(v.Definition.Value, v.Type())
	g.genTemps(w, temps)

	g.genTrivia(w, v.Definition.Tokens.Name)
	g.Fgenf(w, "%s%s = %.v\n", g.Indent, PyName(v.Name()), value)
}

func (g *generator) genOutputVariable(w io.Writer, v *pcl.OutputVariable) {
	value, temps := g.lowerExpression(v.Value, v.Type())
	g.genTemps(w, temps)

	// TODO(pdg): trivia
	g.Fgenf(w, "%spulumi.export(\"%s\", %.v)\n", g.Indent, v.LogicalName(), value)
}

func (g *generator) genNYI(w io.Writer, reason string, vs ...interface{}) {
	message := "not yet implemented: " + fmt.Sprintf(reason, vs...)
	g.diagnostics = append(g.diagnostics, &hcl.Diagnostic{
		Severity: hcl.DiagError,
		Summary:  message,
		Detail:   message,
	})
	g.Fgenf(w, "(lambda: raise Exception(%q))()", fmt.Sprintf(reason, vs...))
}