// Copyright 2016-2021, 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 dotnet

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

	mapset "github.com/deckarep/golang-set/v2"
	"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"
)

type GenerateProgramOptions struct {
	// Determines whether ResourceArg types have an implicit name
	// when constructing a resource. For example:
	// when implicitResourceArgsTypeName is set to true,
	// new Bucket("name", new BucketArgs { ... })
	// becomes
	// new Bucket("name", new() { ... });
	// The latter syntax is only available on .NET 6 or later
	implicitResourceArgsTypeName bool
}

type generator struct {
	// The formatter to use when generating code.
	*format.Formatter
	program *pcl.Program
	// C# namespace map per package.
	namespaces map[string]map[string]string
	// C# codegen compatibility mode per package.
	compatibilities map[string]string
	// A function to convert tokens to module names per package (utilizes the `moduleFormat` setting internally).
	tokenToModules map[string]func(x string) string
	// Type names per invoke function token.
	functionArgs map[string]string
	// keep track of variable identifiers which are the result of an invoke
	// for example "var resourceGroup = GetResourceGroup.Invoke(...)"
	// we will keep track of the reference "resourceGroup"
	//
	// later on when apply a traversal such as resourceGroup.name,
	// we should rewrite it as resourceGroup.Apply(resourceGroupResult => resourceGroupResult.name)
	functionInvokes map[string]*schema.Function
	// Whether awaits are needed, and therefore an async Initialize method should be declared.
	asyncInit            bool
	configCreated        bool
	diagnostics          hcl.Diagnostics
	insideFunctionInvoke bool
	insideAwait          bool
	// Program generation options
	generateOptions GenerateProgramOptions
	isComponent     bool
	// when creating a list of items, we need to know the type of the list
	// if is it a plain list, then `new()` should be used because we are creating List<T>
	// however if we have InputList<T> or anything else, we use `new[]` because InputList<T> can be implicitly casted
	// from an array
	listInitializer string
}

func (g *generator) resetListInitializer() {
	g.listInitializer = "new[]"
}

func (g *generator) usingDefaultListInitializer() bool {
	return g.listInitializer == "new[]"
}

const (
	pulumiPackage = "pulumi"
	dynamicType   = "dynamic"
)

func GenerateProgramWithOptions(
	program *pcl.Program,
	options GenerateProgramOptions,
) (map[string][]byte, hcl.Diagnostics, error) {
	pcl.MapProvidersAsResources(program)
	// Linearize the nodes into an order appropriate for procedural code generation.
	nodes := pcl.Linearize(program)

	// Import C#-specific schema info.
	namespaces := make(map[string]map[string]string)
	compatibilities := make(map[string]string)
	tokenToModules := make(map[string]func(x string) string)
	functionArgs := make(map[string]string)
	packages, err := program.PackageSnapshots()
	if err != nil {
		return nil, nil, err
	}
	for _, p := range packages {
		if err := p.ImportLanguages(map[string]schema.Language{"csharp": Importer}); err != nil {
			return make(map[string][]byte), nil, err
		}

		csharpInfo, hasInfo := p.Language["csharp"].(CSharpPackageInfo)
		if !hasInfo {
			csharpInfo = CSharpPackageInfo{}
		}
		packageNamespaces := csharpInfo.Namespaces
		namespaces[p.Name] = packageNamespaces
		compatibilities[p.Name] = csharpInfo.Compatibility
		tokenToModules[p.Name] = p.TokenToModule

		for _, f := range p.Functions {
			if f.Inputs != nil {
				functionArgs[f.Inputs.Token] = f.Token
			}
		}
	}

	g := &generator{
		program:         program,
		namespaces:      namespaces,
		compatibilities: compatibilities,
		tokenToModules:  tokenToModules,
		functionArgs:    functionArgs,
		functionInvokes: map[string]*schema.Function{},
		generateOptions: options,
		listInitializer: "new[]",
	}

	g.Formatter = format.NewFormatter(g)

	for _, n := range nodes {
		if r, ok := n.(*pcl.Resource); ok && requiresAsyncInit(r) {
			g.asyncInit = true
			break
		}
	}

	var index bytes.Buffer
	g.genPreamble(&index, program)

	g.Indented(func() {
		for _, n := range nodes {
			g.genNode(&index, n)
		}
	})
	g.genPostamble(&index, nodes)

	files := map[string][]byte{
		"Program.cs": index.Bytes(),
	}

	for _, component := range program.CollectComponents() {
		componentName := component.DeclarationName()
		componentNodes := pcl.Linearize(component.Program)

		componentGenerator := &generator{
			program:         component.Program,
			namespaces:      namespaces,
			compatibilities: compatibilities,
			tokenToModules:  tokenToModules,
			functionArgs:    functionArgs,
			functionInvokes: map[string]*schema.Function{},
			generateOptions: options,
			isComponent:     true,
			listInitializer: "new[]",
		}

		componentGenerator.Formatter = format.NewFormatter(componentGenerator)

		var componentBuffer bytes.Buffer
		componentGenerator.genComponentPreamble(&componentBuffer, componentName, component)

		// inside the namespace
		componentGenerator.Indented(func() {
			// inside the class
			componentGenerator.Indented(func() {
				// inside the constructor
				componentGenerator.Indented(func() {
					for _, node := range componentNodes {
						switch node := node.(type) {
						case *pcl.LocalVariable:
							componentGenerator.genLocalVariable(&componentBuffer, node)
						case *pcl.Component:
							// set options { parent = this } for the component resource
							// where "this" 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: "this",
								})
							}
							componentGenerator.genComponent(&componentBuffer, node)
						case *pcl.Resource:
							// set options { parent = this } for the resource
							// where "this" 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: "this",
								})
							}
							componentGenerator.genResource(&componentBuffer, node)
						}
					}
				})
			})
		})

		componentGenerator.genComponentPostamble(&componentBuffer, component)
		files[componentName+".cs"] = componentBuffer.Bytes()
	}

	return files, g.diagnostics, nil
}

func GenerateProgram(program *pcl.Program) (map[string][]byte, hcl.Diagnostics, error) {
	defaultOptions := GenerateProgramOptions{
		// by default, we generate C# code that targets .NET 6
		implicitResourceArgsTypeName: true,
	}

	return GenerateProgramWithOptions(program, defaultOptions)
}

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)
		}
	}

	// Set the runtime to "dotnet" then marshal to Pulumi.yaml
	project.Runtime = workspace.NewProjectRuntimeInfo("dotnet", nil)
	projectBytes, err := encoding.YAML.Marshal(project)
	if err != nil {
		return err
	}
	err = os.WriteFile(path.Join(rootDirectory, "Pulumi.yaml"), projectBytes, 0o600)
	if err != nil {
		return fmt.Errorf("write Pulumi.yaml: %w", err)
	}

	// Build a .csproj based on the packages used by program
	var csproj bytes.Buffer
	csproj.WriteString(`<Project Sdk="Microsoft.NET.Sdk">

	<PropertyGroup>
		<OutputType>Exe</OutputType>
		<TargetFramework>net6.0</TargetFramework>
		<Nullable>enable</Nullable>
	</PropertyGroup>
`)

	// Find all the local dependency folders
	folders := mapset.NewSet[string]()
	for _, dep := range localDependencies {
		folders.Add(path.Dir(dep))
	}
	if len(folders.ToSlice()) > 0 {
		csproj.WriteString(`	<PropertyGroup>
		<RestoreSources>`)
		csproj.WriteString(strings.Join(folders.ToSlice(), ";"))
		csproj.WriteString(`;$(RestoreSources)</RestoreSources>
	</PropertyGroup>
`)
	}

	csproj.WriteString("	<ItemGroup>\n")

	// Add the Pulumi package reference
	if path, has := localDependencies[pulumiPackage]; has {
		filename := filepath.Base(path)
		pkg, rest, _ := strings.Cut(filename, ".")
		version, _ := strings.CutSuffix(rest, ".nupkg")

		csproj.WriteString(fmt.Sprintf(
			"		<PackageReference Include=\"%s\" Version=\"%s\" />\n",
			pkg, version))
	} else {
		csproj.WriteString("		<PackageReference Include=\"Pulumi\" Version=\"3.*\" />\n")
	}

	// For each package add a PackageReference line
	packages, err := program.CollectNestedPackageSnapshots()
	if err != nil {
		return err
	}
	for _, p := range packages {
		packageTemplate := "		<PackageReference Include=\"%s\" Version=\"%s\" />\n"

		if err := p.ImportLanguages(map[string]schema.Language{"csharp": Importer}); err != nil {
			return err
		}
		if p.Name == pulumiPackage {
			continue
		}

		packageName := "Pulumi." + namespaceName(map[string]string{}, p.Name)
		if langInfo, found := p.Language["csharp"]; found {
			csharpInfo, ok := langInfo.(CSharpPackageInfo)
			if ok {
				namespace := namespaceName(csharpInfo.Namespaces, p.Name)
				packageName = fmt.Sprintf("%s.%s", csharpInfo.GetRootNamespace(), namespace)
			}
		}
		if p.Version != nil {
			fmt.Fprintf(&csproj, packageTemplate, packageName, p.Version.String())
		} else {
			fmt.Fprintf(&csproj, packageTemplate, packageName, "*")
		}
	}

	csproj.WriteString(`	</ItemGroup>

</Project>`)

	files[project.Name.String()+".csproj"] = csproj.Bytes()

	// Add the language specific .gitignore
	files[".gitignore"] = []byte(dotnetGitIgnore)

	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)
		}
	}

	return nil
}

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

func (g *generator) findFunctionSchema(token string, location *hcl.Range) (*schema.Function, bool) {
	for _, pkg := range g.program.PackageReferences() {
		fn, ok, err := pcl.LookupFunction(pkg, token)
		if !ok {
			continue
		}
		if err != nil {
			g.diagnostics = append(g.diagnostics, &hcl.Diagnostic{
				Severity: hcl.DiagWarning,
				Summary:  fmt.Sprintf("Could not find function schema for '%s'", token),
				Detail:   err.Error(),
				Subject:  location,
			})
			return nil, false
		}
		return fn, true
	}
	return nil, false
}

func (g *generator) isFunctionInvoke(localVariable *pcl.LocalVariable) (*schema.Function, bool) {
	value := localVariable.Definition.Value
	switch value.(type) {
	case *model.FunctionCallExpression:
		call := value.(*model.FunctionCallExpression)
		switch call.Name {
		case pcl.Invoke:
			token := call.Args[0].(*model.TemplateExpression).Parts[0].(*model.LiteralValueExpression).Value.AsString()
			return g.findFunctionSchema(token, call.Args[0].SyntaxNode().Range().Ptr())
		}
	}

	return nil, false
}

// 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)
	}
}

type programUsings struct {
	systemUsings        codegen.StringSet
	pulumiUsings        codegen.StringSet
	pulumiHelperMethods codegen.StringSet
}

func (g *generator) usingStatements(program *pcl.Program) programUsings {
	systemUsings := codegen.NewStringSet("System.Linq", "System.Collections.Generic")
	pulumiUsings := codegen.NewStringSet()
	preambleHelperMethods := codegen.NewStringSet()
	for _, n := range program.Nodes {
		if r, isResource := n.(*pcl.Resource); isResource {
			pcl.FixupPulumiPackageTokens(r)
			pkg, _, _, _ := r.DecomposeToken()
			if pkg != pulumiPackage {
				namespace := namespaceName(g.namespaces[pkg], pkg)
				var info CSharpPackageInfo
				if r.Schema != nil && r.Schema.PackageReference != nil {
					def, err := r.Schema.PackageReference.Definition()
					contract.AssertNoErrorf(err, "error loading definition for package %q", r.Schema.PackageReference.Name())
					if csharpinfo, ok := def.Language["csharp"].(CSharpPackageInfo); ok {
						info = csharpinfo
					}
				}
				pulumiUsings.Add(fmt.Sprintf("%s = %[2]s.%[1]s", namespace, info.GetRootNamespace()))
			}
		}
		diags := n.VisitExpressions(nil, func(n model.Expression) (model.Expression, hcl.Diagnostics) {
			if call, ok := n.(*model.FunctionCallExpression); ok {
				for _, i := range g.genFunctionUsings(call) {
					if strings.HasPrefix(i, "System") {
						systemUsings.Add(i)
					} else {
						pulumiUsings.Add(i)
					}
				}

				// Checking to see if this function call deserves its own dedicated helper method in the preamble
				if helperMethodBody, ok := getHelperMethodIfNeeded(call.Name, g.Indent); ok {
					preambleHelperMethods.Add(helperMethodBody)
				}
			}
			if _, ok := n.(*model.SplatExpression); ok {
				systemUsings.Add("System.Linq")
			}
			return n, nil
		})
		contract.Assertf(len(diags) == 0, "unexpected diagnostics: %v", diags)
	}

	return programUsings{
		systemUsings:        systemUsings,
		pulumiUsings:        pulumiUsings,
		pulumiHelperMethods: preambleHelperMethods,
	}
}

func configObjectTypeName(variableName string) string {
	return Title(variableName) + "Args"
}

func componentInputElementType(pclType model.Type) string {
	switch pclType {
	case model.BoolType:
		return "bool"
	case model.IntType:
		return "int"
	case model.NumberType:
		return "double"
	case model.StringType:
		return "string"
	default:
		switch pclType := pclType.(type) {
		case *model.ListType, *model.MapType:
			return componentInputType(pclType)
		// reduce option(T) to just T
		// the generated args class assumes 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 dynamicType
			}
		default:
			return dynamicType
		}
	}
}

func componentInputType(pclType model.Type) string {
	switch pclType := pclType.(type) {
	case *model.ListType:
		elementType := componentInputElementType(pclType.ElementType)
		return fmt.Sprintf("InputList<%s>", elementType)
	case *model.MapType:
		elementType := componentInputElementType(pclType.ElementType)
		return fmt.Sprintf("InputMap<%s>", elementType)
	default:
		elementType := componentInputElementType(pclType)
		return fmt.Sprintf("Input<%s>", elementType)
	}
}

func componentOutputElementType(pclType model.Type) string {
	switch pclType {
	case model.BoolType:
		return "bool"
	case model.IntType:
		return "int"
	case model.NumberType:
		return "double"
	case model.StringType:
		return "string"
	default:
		switch pclType := pclType.(type) {
		case *model.ListType:
			elementType := componentOutputElementType(pclType.ElementType)
			return fmt.Sprintf("List<%s>", elementType)
		case *model.MapType:
			elementType := componentOutputElementType(pclType.ElementType)
			return fmt.Sprintf("Dictionary<string, %s>", elementType)
		case *model.OutputType:
			// something is already an output
			// get only the element type because we are wrapping these in Output<T> anyway
			return componentOutputElementType(pclType.ElementType)
		default:
			return dynamicType
		}
	}
}

func mainConfigElementType(pclType model.Type) string {
	pclType = pcl.UnwrapOption(pclType)
	switch pclType {
	case model.BoolType:
		return "bool"
	case model.IntType:
		return "int"
	case model.NumberType:
		return "double"
	case model.StringType:
		return "string"
	default:
		switch pclType := pclType.(type) {
		case *model.ListType:
			elementType := mainConfigElementType(pclType.ElementType)
			return fmt.Sprintf("List<%s>", elementType)
		case *model.MapType:
			elementType := mainConfigElementType(pclType.ElementType)
			return fmt.Sprintf("Dictionary<string, %s>", elementType)
		default:
			return dynamicType
		}
	}
}

func componentOutputType(pclType model.Type) string {
	elementType := componentOutputElementType(pclType)
	return fmt.Sprintf("Output<%s>", elementType)
}

type ObjectTypeFromConfigMetadata = struct {
	TypeName      string
	ComponentName string
}

func annotateObjectTypedConfig(componentName string, typeName string, objectType *model.ObjectType) *model.ObjectType {
	objectType.Annotations = append(objectType.Annotations, &ObjectTypeFromConfigMetadata{
		TypeName:      typeName,
		ComponentName: componentName,
	})

	return objectType
}

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

	return objectTypes
}

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

	return objectTypes
}

func (g *generator) genComponentPreamble(w io.Writer, componentName string, component *pcl.Component) {
	// Accumulate other using statements for the various providers and packages. Don't emit them yet, as we need
	// to sort them later on.
	programUsings := g.usingStatements(component.Program)
	systemUsings := programUsings.systemUsings
	pulumiUsings := programUsings.pulumiUsings
	for _, pkg := range systemUsings.SortedValues() {
		g.Fprintf(w, "using %v;\n", pkg)
	}
	g.Fprintln(w, `using Pulumi;`)
	for _, pkg := range pulumiUsings.SortedValues() {
		g.Fprintf(w, "using %v;\n", pkg)
	}
	configVars := component.Program.ConfigVariables()

	g.Fprint(w, "\n")
	g.Fprintln(w, "namespace Components")
	g.Fprintf(w, "{\n")
	g.Indented(func() {
		if len(configVars) > 0 {
			g.Fprintf(w, "%spublic class %sArgs : global::Pulumi.ResourceArgs\n", g.Indent, componentName)
			g.Fprintf(w, "%s{\n", g.Indent)
			g.Indented(func() {
				objectTypedConfigVars := collectComponentObjectTypedConfigVariables(component)
				variableNames := pcl.SortedStringKeys(objectTypedConfigVars)
				// generate resource args for this component
				for _, variableName := range variableNames {
					objectType := objectTypedConfigVars[variableName]
					objectTypeName := configObjectTypeName(variableName)
					g.Fprintf(w, "%spublic class %s : global::Pulumi.ResourceArgs\n", g.Indent, objectTypeName)
					g.Fprintf(w, "%s{\n", g.Indent)
					g.Indented(func() {
						propertyNames := pcl.SortedStringKeys(objectType.Properties)
						for _, propertyName := range propertyNames {
							propertyType := objectType.Properties[propertyName]
							inputType := componentInputType(propertyType)
							g.Fprintf(w, "%s[Input(\"%s\")]\n", g.Indent, propertyName)
							g.Fprintf(w, "%spublic %s? %s { get; set; }\n",
								g.Indent,
								inputType,
								Title(propertyName))
						}
					})
					g.Fprintf(w, "%s}\n\n", g.Indent)
				}

				for _, configVar := range configVars {
					// for simple values, get the primitive type
					inputType := componentInputType(configVar.Type())
					switch configType := configVar.Type().(type) {
					case *model.ObjectType:
						// for objects of type T, generate T as is
						inputType = configObjectTypeName(configVar.Name())
					case *model.ListType:
						// for list(T) where T is an object type, generate T[]
						switch configType.ElementType.(type) {
						case *model.ObjectType:
							objectTypeName := configObjectTypeName(configVar.Name())
							inputType = objectTypeName + "[]"
						}
					case *model.MapType:
						// for map(T) where T is an object type, generate Dictionary<string, T>
						switch configType.ElementType.(type) {
						case *model.ObjectType:
							objectTypeName := configObjectTypeName(configVar.Name())
							inputType = fmt.Sprintf("Dictionary<string, %s>", objectTypeName)
						}
					}

					if configVar.Description != "" {
						g.Fgenf(w, "%s/// <summary>\n", g.Indent)
						for _, line := range strings.Split(configVar.Description, "\n") {
							g.Fgenf(w, "%s/// %s\n", g.Indent, line)
						}
						g.Fgenf(w, "%s/// </summary>\n", g.Indent)
					}
					g.Fprintf(w, "%s[Input(\"%s\")]\n", g.Indent, configVar.LogicalName())
					g.Fprintf(w, "%spublic %s %s { get; set; } = ",
						g.Indent,
						inputType,
						Title(configVar.Name()))

					if configVar.DefaultValue != nil {
						g.Fprintf(w, "%v;\n", g.lowerExpression(configVar.DefaultValue, configVar.DefaultValue.Type()))
					} else {
						g.Fprint(w, "null!;\n")
					}
				}
			})
			g.Fprintf(w, "%s}\n\n", g.Indent)
		}

		g.Fprintf(w, "%spublic class %s : global::Pulumi.ComponentResource\n", g.Indent, componentName)
		g.Fprintf(w, "%s{\n", g.Indent)
		g.Indented(func() {
			for _, outputVar := range component.Program.OutputVariables() {
				var outputType string
				switch expr := outputVar.Value.(type) {
				case *model.ScopeTraversalExpression:
					resource, ok := expr.Parts[0].(*pcl.Resource)
					if ok && len(expr.Parts) == 1 {
						// special case: the output is a Resource type
						outputType = fmt.Sprintf("Output<%s>", g.resourceTypeName(resource))
					} else {
						outputType = componentOutputType(expr.Type())
					}
				default:
					outputType = componentOutputType(expr.Type())
				}

				g.Fprintf(w, "%s[Output(\"%s\")]\n", g.Indent, outputVar.LogicalName())
				g.Fprintf(w, "%spublic %s %s { get; private set; }\n",
					g.Indent,
					outputType,
					Title(outputVar.Name()))
			}

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

			token := "components:index:" + componentName
			if len(configVars) == 0 {
				// There is no args class
				g.Fgenf(w, "%spublic %s(string name, ComponentResourceOptions? opts = null)\n",
					g.Indent,
					componentName)

				g.Fgenf(w, "%s    : base(\"%s\", name, ResourceArgs.Empty, opts)\n", g.Indent, token)
			} else {
				// There is no args class
				g.Fgenf(w, "%spublic %s(string name, %sArgs args, ComponentResourceOptions? opts = null)\n",
					g.Indent,
					componentName,
					componentName)

				g.Fgenf(w, "%s    : base(\"%s\", name, args, opts)\n", g.Indent, token)
			}

			g.Fgenf(w, "%s{\n", g.Indent)
		})
	})
}

func (g *generator) genComponentPostamble(w io.Writer, component *pcl.Component) {
	outputVars := component.Program.OutputVariables()
	g.Indented(func() {
		g.Indented(func() {
			g.Indented(func() {
				if len(outputVars) == 0 {
					g.Fgenf(w, "%sthis.RegisterOutputs();\n", g.Indent)
				} else {
					// Emit component resource output assignment
					for _, output := range outputVars {
						outputProperty := Title(output.Name())
						switch expr := output.Value.(type) {
						case *model.ScopeTraversalExpression:
							_, ok := expr.Parts[0].(*pcl.Resource)
							if ok && len(expr.Parts) == 1 {
								// special case: the output is a Resource type
								g.Fgenf(w, "%sthis.%s = Output.Create(%.3v);\n",
									g.Indent, outputProperty,
									g.lowerExpression(output.Value, output.Type()))
							} else {
								g.Fgenf(w, "%sthis.%s = %.3v;\n",
									g.Indent, outputProperty,
									g.lowerExpression(output.Value, output.Type()))
							}
						default:

							g.Fgenf(w, "%sthis.%s = %.3v;\n",
								g.Indent, outputProperty,
								g.lowerExpression(output.Value, output.Type()))
						}
					}

					g.Fgen(w, "\n")

					g.Fgenf(w, "%sthis.RegisterOutputs(new Dictionary<string, object?>\n", g.Indent)
					g.Fgenf(w, "%s{\n", g.Indent)
					g.Indented(func() {
						// Emit component resource output properties
						for _, n := range outputVars {
							outputID := fmt.Sprintf(`"%s"`, g.escapeString(n.LogicalName(), false, false))
							g.Fgenf(w, "%s[%s] = %.3v,\n", g.Indent, outputID, g.lowerExpression(n.Value, n.Type()))
						}
					})
					g.Fgenf(w, "%s});\n", g.Indent)
				}
			})
		})
	})

	// closing bracket for the component resource class constructor
	indent := "    "
	g.Fprintf(w, "%s%s}\n", indent, indent)
	// closing bracket for the component resource class
	g.Fprintf(w, "%s}\n", indent)
	// closing bracket for the components namespace
	g.Fprint(w, "}\n")
}

// genPreamble generates using statements, class definition and constructor.
func (g *generator) genPreamble(w io.Writer, program *pcl.Program) {
	// Accumulate other using statements for the various providers and packages. Don't emit them yet, as we need
	// to sort them later on.
	programUsings := g.usingStatements(program)
	systemUsings := programUsings.systemUsings
	pulumiUsings := programUsings.pulumiUsings
	preambleHelperMethods := programUsings.pulumiHelperMethods

	if g.asyncInit {
		systemUsings.Add("System.Threading.Tasks")
	}

	for _, pkg := range systemUsings.SortedValues() {
		g.Fprintf(w, "using %v;\n", pkg)
	}
	g.Fprintln(w, `using Pulumi;`)
	for _, pkg := range pulumiUsings.SortedValues() {
		g.Fprintf(w, "using %v;\n", pkg)
	}

	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, "\t%s\n\n", preambleHelperMethodBody)
	}

	asyncKeywordWhenNeeded := ""
	if g.asyncInit {
		asyncKeywordWhenNeeded = "async"
	}
	g.Fprintf(w, "return await Deployment.RunAsync(%s() => \n", asyncKeywordWhenNeeded)
	g.Fprint(w, "{\n")
}

// hasOutputVariables checks whether there are any output declarations
func hasOutputVariables(nodes []pcl.Node) bool {
	for _, n := range nodes {
		switch n.(type) {
		case *pcl.OutputVariable:
			return true
		}
	}

	return false
}

// genPostamble closes the method and the class and declares stack output statements.
func (g *generator) genPostamble(w io.Writer, nodes []pcl.Node) {
	if hasOutputVariables(nodes) {
		g.Indented(func() {
			g.Fgenf(w, "%sreturn new Dictionary<string, object?>\n", g.Indent)
			g.Fgenf(w, "%s{\n", g.Indent)
			g.Indented(func() {
				// Emit stack output properties
				for _, n := range nodes {
					switch n := n.(type) {
					case *pcl.OutputVariable:
						outputID := fmt.Sprintf(`"%s"`, g.escapeString(n.LogicalName(), false, false))
						g.Fgenf(w, "%s[%s] = %.3v,\n", g.Indent, outputID, g.lowerExpression(n.Value, n.Type()))
					}
				}
			})
			g.Fgenf(w, "%s};\n", g.Indent)
		})
	}
	// Close lambda call expression
	g.Fprintf(w, "});\n\n")

	// Generate types for object typed config variables
	// those are referenced in config.GetObject<T> where T is one of these generated types
	// they must be generated after the top-level statement call to Deployment.RunAsync
	objectTypedConfigVariables := collectObjectTypedConfigVariables(g.program)
	objectTypeKeys := pcl.SortedStringKeys(objectTypedConfigVariables)
	for _, typeName := range objectTypeKeys {
		objectType := objectTypedConfigVariables[typeName]
		g.Fgenf(w, "public class %s\n{\n", typeName)
		sortedProperties := pcl.SortedStringKeys(objectType.Properties)
		for _, propertyName := range sortedProperties {
			g.Indented(func() {
				property := objectType.Properties[propertyName]
				propertyType := mainConfigElementType(property)
				g.Fgenf(w, "%spublic %s %s { get; set; }\n", g.Indent, propertyType, propertyName)
			})
		}
		g.Fgenf(w, "}\n\n")
	}
}

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.Component:
		g.genComponent(w, n)
	}
}

// requiresAsyncInit returns true if the program requires awaits in the code, and therefore an asynchronous
// method must be declared.
func requiresAsyncInit(r *pcl.Resource) bool {
	if r.Options == nil || r.Options.Range == nil {
		return false
	}

	return model.ContainsPromises(r.Options.Range.Type())
}

// resourceTypeName computes the C# class name for the given resource.
func (g *generator) resourceTypeName(r *pcl.Resource) string {
	pcl.FixupPulumiPackageTokens(r)
	// Compute the resource type from the Pulumi type token.
	pkg, module, member, diags := r.DecomposeToken()
	contract.Assertf(len(diags) == 0, "error decomposing token: %v", diags)

	if r.Schema != nil {
		if val1, ok := r.Schema.Language["csharp"]; ok {
			val2, ok := val1.(CSharpResourceInfo)
			contract.Assertf(ok, "dotnet specific settings for resources should be of type CSharpResourceInfo")
			member = val2.Name
		}
	}

	namespaces := g.namespaces[pkg]
	rootNamespace := namespaceName(namespaces, pkg)

	namespace := namespaceName(namespaces, module)
	namespaceTokens := strings.Split(namespace, "/")
	for i, name := range namespaceTokens {
		namespaceTokens[i] = Title(name)
	}
	namespace = strings.Join(namespaceTokens, ".")

	if namespace != "" {
		namespace = "." + namespace
	}

	qualifiedMemberName := fmt.Sprintf("%s%s.%s", rootNamespace, namespace, Title(member))
	return qualifiedMemberName
}

func (g *generator) extractInputPropertyNameMap(r *pcl.Resource) map[string]string {
	// Extract language-specific property names from schema
	csharpInputPropertyNameMap := make(map[string]string)
	if r.Schema != nil {
		for _, inputProperty := range r.Schema.InputProperties {
			if val1, ok := inputProperty.Language["csharp"]; ok {
				if val2, ok := val1.(CSharpPropertyInfo); ok {
					csharpInputPropertyNameMap[inputProperty.Name] = val2.Name
				}
			}
		}
	}
	return csharpInputPropertyNameMap
}

// resourceArgsTypeName computes the C# arguments class name for the given resource.
func (g *generator) resourceArgsTypeName(r *pcl.Resource) string {
	// Compute the resource type from the Pulumi type token.
	pkg, module, member, diags := r.DecomposeToken()
	contract.Assertf(len(diags) == 0, "error decomposing token: %v", diags)

	namespaces := g.namespaces[pkg]
	rootNamespace := namespaceName(namespaces, pkg)
	namespace := namespaceName(namespaces, module)
	if g.compatibilities[pkg] == "kubernetes20" && module != "" {
		namespace = "Types.Inputs." + namespace
	}

	if namespace != "" {
		namespace = "." + namespace
	}

	return fmt.Sprintf("%s%s.%sArgs", rootNamespace, namespace, Title(member))
}

// functionName computes the C# namespace and class name for the given function token.
func (g *generator) functionName(tokenArg model.Expression) (string, string) {
	token := tokenArg.(*model.TemplateExpression).Parts[0].(*model.LiteralValueExpression).Value.AsString()
	tokenRange := tokenArg.SyntaxNode().Range()

	// Compute the resource type from the Pulumi type token.
	pkg, module, member, diags := pcl.DecomposeToken(token, tokenRange)
	contract.Assertf(len(diags) == 0, "error decomposing token: %v", diags)
	namespaces := g.namespaces[pkg]
	rootNamespace := namespaceName(namespaces, pkg)
	namespace := namespaceName(namespaces, module)

	if namespace != "" {
		namespace = "." + namespace
	}

	return rootNamespace, fmt.Sprintf("%s%s.%s", rootNamespace, namespace, Title(member))
}

func (g *generator) toSchemaType(destType model.Type) (schema.Type, bool) {
	schemaType, ok := pcl.GetSchemaForType(destType)
	if !ok {
		return nil, false
	}
	return codegen.UnwrapType(schemaType), true
}

// argumentTypeName computes the C# argument class name for the given expression and model type.
func (g *generator) argumentTypeName(expr model.Expression, destType model.Type) string {
	suffix := "Args"
	if g.insideFunctionInvoke {
		suffix = "InputArgs"
	}
	return g.argumentTypeNameWithSuffix(expr, destType, suffix)
}

func (g *generator) argumentTypeNameWithSuffix(expr model.Expression, destType model.Type, suffix string) string {
	schemaType, ok := g.toSchemaType(destType)
	if !ok {
		return ""
	}

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

	token := objType.Token
	tokenRange := expr.SyntaxNode().Range()
	qualifier := "Inputs"
	if f, ok := g.functionArgs[token]; ok {
		token = f
		qualifier = ""
	}

	pkg, modName, member, diags := pcl.DecomposeToken(token, tokenRange)
	contract.Assertf(len(diags) == 0, "error decomposing token: %v", diags)
	var module string
	if getModule, ok := g.tokenToModules[pkg]; ok {
		module = getModule(token)
	} else {
		module = strings.SplitN(modName, "/", 2)[0]
	}
	namespaces := g.namespaces[pkg]
	rootNamespace := namespaceName(namespaces, pkg)
	namespace := namespaceName(namespaces, module)
	if strings.ToLower(namespace) == "index" {
		namespace = ""
	}
	if namespace != "" {
		namespace = "." + namespace
	}
	if g.compatibilities[pkg] == "kubernetes20" {
		namespace = ".Types.Inputs" + namespace
	} else if qualifier != "" {
		namespace = namespace + "." + qualifier
	}
	member = member + suffix

	return fmt.Sprintf("%s%s.%s", rootNamespace, namespace, Title(member))
}

// 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(`$"{name}-%s"`, baseName)
		}

		return fmt.Sprintf(`"%s"`, baseName)
	}

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

	return fmt.Sprintf("$\"%s-{%s}\"", baseName, count)
}

func (g *generator) genResourceOptions(opts *pcl.ResourceOptions, resourceOptionsType string) string {
	if opts == nil {
		return ""
	}
	var result bytes.Buffer
	appendOption := func(name string, value model.Expression) {
		if result.Len() == 0 {
			_, err := fmt.Fprintf(&result, ", new %s\n%s{", resourceOptionsType, g.Indent)
			g.Indent += "    "
			contract.IgnoreError(err)
		}

		if name == "IgnoreChanges" {
			// ignore changes need to be special cased
			// because new [] { "field" } cannot be implicitly casted to List<string>
			// which is the type of IgnoreChanges
			if changes, isTuple := value.(*model.TupleConsExpression); isTuple {
				g.Fgenf(&result, "\n%sIgnoreChanges =", g.Indent)
				g.Fgenf(&result, "\n%s{", g.Indent)
				g.Indented(func() {
					for _, v := range changes.Expressions {
						g.Fgenf(&result, "\n%s\"%.v\",", g.Indent, v)
					}
				})
				g.Fgenf(&result, "\n%s},", g.Indent)
			} else {
				g.Fgenf(&result, "\n%s%s = %v,", g.Indent, name, g.lowerExpression(value, value.Type()))
			}
		} else if name == "Parent" {
			// special case parent = this, do not escape "this"
			if parent, isThis := value.(*model.ScopeTraversalExpression); isThis {
				if parent.RootName == "this" && len(parent.Parts) == 1 && g.isComponent {
					g.Fgenf(&result, "\n%s%s = this,", g.Indent, name)
				} else {
					g.Fgenf(&result, "\n%s%s = %v,", g.Indent, name, g.lowerExpression(value, value.Type()))
				}
			} else {
				g.Fgenf(&result, "\n%s%s = %v,", g.Indent, name, g.lowerExpression(value, value.Type()))
			}
		} else if name == "DependsOn" {
			// depends on need to be special cased
			// because new [] { resourceA, resourceB } cannot be implicitly casted to InputList<Resource>
			// use syntax DependsOn = { resourceA, resourceB } instead
			if resourcesList, isTuple := value.(*model.TupleConsExpression); isTuple {
				g.Fgenf(&result, "\n%sDependsOn =", g.Indent)
				g.Fgenf(&result, "\n%s{", g.Indent)
				g.Indented(func() {
					for _, resource := range resourcesList.Expressions {
						g.Fgenf(&result, "\n%s%v,", g.Indent, resource)
					}
				})
				g.Fgenf(&result, "\n%s},", g.Indent)
			} else {
				g.Fgenf(&result, "\n%s%s = %v,", g.Indent, name, g.lowerExpression(value, value.Type()))
			}
		} else {
			g.Fgenf(&result, "\n%s%s = %v,", g.Indent, name, g.lowerExpression(value, value.Type()))
		}
	}

	if opts.Parent != nil {
		appendOption("Parent", opts.Parent)
	}
	if opts.Provider != nil {
		appendOption("Provider", opts.Provider)
	}
	if opts.DependsOn != nil {
		appendOption("DependsOn", opts.DependsOn)
	}
	if opts.Protect != nil {
		appendOption("Protect", opts.Protect)
	}
	if opts.RetainOnDelete != nil {
		appendOption("RetainOnDelete", opts.RetainOnDelete)
	}
	if opts.IgnoreChanges != nil {
		appendOption("IgnoreChanges", opts.IgnoreChanges)
	}
	if opts.DeletedWith != nil {
		appendOption("DeletedWith", opts.DeletedWith)
	}

	if result.Len() != 0 {
		g.Indent = g.Indent[:len(g.Indent)-4]
		_, err := fmt.Fprintf(&result, "\n%s}", g.Indent)
		contract.IgnoreError(err)
	}

	return result.String()
}

func AnnotateComponentInputs(component *pcl.Component) {
	componentName := component.DeclarationName()
	configVars := component.Program.ConfigVariables()

	for index := range component.Inputs {
		attribute := component.Inputs[index]
		switch expr := attribute.Value.(type) {
		case *model.ObjectConsExpression:
			for _, configVar := range configVars {
				if configVar.Name() == attribute.Name {
					switch configVar.Type().(type) {
					case *model.ObjectType:
						expr.WithType(func(objectExprType model.Type) *model.ObjectConsExpression {
							switch exprType := objectExprType.(type) {
							case *model.ObjectType:
								typeName := configObjectTypeName(configVar.Name())
								annotateObjectTypedConfig(componentName, typeName, exprType)
							}

							return expr
						})
					case *model.MapType:
						for _, item := range expr.Items {
							switch mapValue := item.Value.(type) {
							case *model.ObjectConsExpression:
								mapValue.WithType(func(objectExprType model.Type) *model.ObjectConsExpression {
									switch exprType := objectExprType.(type) {
									case *model.ObjectType:
										typeName := configObjectTypeName(configVar.Name())
										annotateObjectTypedConfig(componentName, typeName, exprType)
									}

									return mapValue
								})
							}
						}
					}
				}
			}
		case *model.TupleConsExpression:
			for _, configVar := range configVars {
				if configVar.Name() == attribute.Name {
					switch listType := configVar.Type().(type) {
					case *model.ListType:
						switch listType.ElementType.(type) {
						case *model.ObjectType:
							for _, item := range expr.Expressions {
								switch itemExpr := item.(type) {
								case *model.ObjectConsExpression:
									itemExpr.WithType(func(objectExprType model.Type) *model.ObjectConsExpression {
										switch exprType := objectExprType.(type) {
										case *model.ObjectType:
											typeName := configObjectTypeName(configVar.Name())
											annotateObjectTypedConfig(componentName, typeName, exprType)
										}
										return itemExpr
									})
								}
							}
						}
					}
				}
			}

		}
	}
}

func isPlainResourceProperty(r *pcl.Resource, name string) bool {
	if r.Schema == nil {
		return false
	}

	for _, property := range r.Schema.InputProperties {
		if property.Name == name {
			return property.Plain
		}
	}
	return false
}

// genResource handles the generation of instantiations of non-builtin resources.
func (g *generator) genResource(w io.Writer, r *pcl.Resource) {
	qualifiedMemberName := g.resourceTypeName(r)
	csharpInputPropertyNameMap := g.extractInputPropertyNameMap(r)

	// Add conversions to input properties
	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...)
			input.Value = g.lowerExpression(input.Value, destType.(model.Type))
			if csharpName, ok := csharpInputPropertyNameMap[input.Name]; ok {
				input.Name = csharpName
			}
		}
	}

	pcl.AnnotateResourceInputs(r)
	name := r.LogicalName()
	variableName := makeValidIdentifier(r.Name())
	argsName := g.resourceArgsTypeName(r)
	g.genTrivia(w, r.Definition.Tokens.GetType(""))
	for _, l := range r.Definition.Tokens.GetLabels(nil) {
		g.genTrivia(w, l)
	}
	g.genTrivia(w, r.Definition.Tokens.GetOpenBrace())

	instantiate := func(resName string) {
		if len(r.Inputs) == 0 && r.Options == nil {
			// only resource name is provided
			g.Fgenf(w, "new %s(%s)", qualifiedMemberName, resName)
		} else {
			if g.generateOptions.implicitResourceArgsTypeName {
				g.Fgenf(w, "new %s(%s, new()\n", qualifiedMemberName, resName)
			} else {
				g.Fgenf(w, "new %s(%s, new %s\n", qualifiedMemberName, resName, argsName)
			}

			g.Fgenf(w, "%s{\n", g.Indent)
			g.Indented(func() {
				for _, attr := range r.Inputs {
					g.Fgenf(w, "%s%s =", g.Indent, propertyName(attr.Name))
					if isPlainResourceProperty(r, attr.Name) {
						g.listInitializer = "new()"
					}

					g.Fgenf(w, " %.v,\n", attr.Value)
					g.resetListInitializer()
				}
			})
			g.Fgenf(w, "%s}%s)", g.Indent, g.genResourceOptions(r.Options, "CustomResourceOptions"))
		}
	}

	if r.Options != nil && r.Options.Range != nil {
		rangeType := model.ResolveOutputs(r.Options.Range.Type())
		rangeExpr := g.lowerExpression(r.Options.Range, rangeType)

		g.Fgenf(w, "%svar %s = new List<%s>();\n", g.Indent, variableName, qualifiedMemberName)

		resKey := "Key"
		if model.InputType(model.NumberType).ConversionFrom(rangeExpr.Type()) != model.NoConversion {
			g.Fgenf(w, "%sfor (var rangeIndex = 0; rangeIndex < %.12o; rangeIndex++)\n", g.Indent, rangeExpr)
			g.Fgenf(w, "%s{\n", g.Indent)
			g.Fgenf(w, "%s    var range = new { Value = rangeIndex };\n", g.Indent)
			resKey = "Value"
		} else {
			rangeExpr := &model.FunctionCallExpression{
				Name: "entries",
				Args: []model.Expression{rangeExpr},
			}
			g.Fgenf(w, "%sforeach (var range in %.v)\n", g.Indent, rangeExpr)
			g.Fgenf(w, "%s{\n", g.Indent)
		}

		resName := g.makeResourceName(name, "range."+resKey)
		g.Indented(func() {
			g.Fgenf(w, "%s%s.Add(", g.Indent, variableName)
			instantiate(resName)
			g.Fgenf(w, ");\n")
		})
		g.Fgenf(w, "%s}\n", g.Indent)
	} else {
		g.Fgenf(w, "%svar %s = ", g.Indent, variableName)
		instantiate(g.makeResourceName(name, ""))
		g.Fgenf(w, ";\n\n")
	}

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

// genComponent handles the generation of instantiations of non-builtin resources.
func (g *generator) genComponent(w io.Writer, r *pcl.Component) {
	componentName := r.DeclarationName()
	qualifiedMemberName := "Components." + componentName

	name := r.LogicalName()
	variableName := makeValidIdentifier(r.Name())
	argsName := componentName + "Args"

	AnnotateComponentInputs(r)

	configVars := r.Program.ConfigVariables()

	instantiate := func(resName string) {
		if len(configVars) == 0 {
			// there is no args type for this component
			g.Fgenf(w, "new %s(%s%s)",
				qualifiedMemberName,
				resName,
				g.genResourceOptions(r.Options, "ComponentResourceOptions"))

			return
		}

		if len(r.Inputs) == 0 && r.Options == nil {
			// only resource name is provided
			g.Fgenf(w, "new %s(%s)", qualifiedMemberName, resName)
		} else {
			if g.generateOptions.implicitResourceArgsTypeName {
				g.Fgenf(w, "new %s(%s, new()\n", qualifiedMemberName, resName)
			} else {
				g.Fgenf(w, "new %s(%s, new %s\n", qualifiedMemberName, resName, argsName)
			}

			g.Fgenf(w, "%s{\n", g.Indent)
			g.Indented(func() {
				for _, attr := range r.Inputs {
					g.Fgenf(w, "%s%s =", g.Indent, propertyName(attr.Name))
					g.Fgenf(w, " %.v,\n", attr.Value)
				}
			})
			g.Fgenf(w, "%s}%s)", g.Indent, g.genResourceOptions(r.Options, "ComponentResourceOptions"))
		}
	}

	if r.Options != nil && r.Options.Range != nil {
		rangeType := model.ResolveOutputs(r.Options.Range.Type())
		rangeExpr := g.lowerExpression(r.Options.Range, rangeType)

		g.Fgenf(w, "%svar %s = new List<%s>();\n", g.Indent, variableName, qualifiedMemberName)

		resKey := "Key"
		if model.InputType(model.NumberType).ConversionFrom(rangeExpr.Type()) != model.NoConversion {
			g.Fgenf(w, "%sfor (var rangeIndex = 0; rangeIndex < %.12o; rangeIndex++)\n", g.Indent, rangeExpr)
			g.Fgenf(w, "%s{\n", g.Indent)
			g.Fgenf(w, "%s    var range = new { Value = rangeIndex };\n", g.Indent)
			resKey = "Value"
		} else {
			rangeExpr := &model.FunctionCallExpression{
				Name: "entries",
				Args: []model.Expression{rangeExpr},
			}
			g.Fgenf(w, "%sforeach (var range in %.v)\n", g.Indent, rangeExpr)
			g.Fgenf(w, "%s{\n", g.Indent)
		}

		resName := g.makeResourceName(name, "range."+resKey)
		g.Indented(func() {
			g.Fgenf(w, "%s%s.Add(", g.Indent, variableName)
			instantiate(resName)
			g.Fgenf(w, ");\n")
		})
		g.Fgenf(w, "%s}\n", g.Indent)
	} else {
		g.Fgenf(w, "%svar %s = ", g.Indent, variableName)
		instantiate(g.makeResourceName(name, ""))
		g.Fgenf(w, ";\n\n")
	}

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

func computeConfigTypeParam(configName string, configType model.Type) string {
	typeName := Title(makeValidIdentifier(configName))
	configType = pcl.UnwrapOption(configType)
	switch configType {
	case model.StringType:
		return "string"
	case model.IntType:
		return "int"
	case model.NumberType:
		return "double"
	case model.BoolType:
		return "bool"
	case model.DynamicType:
		return "dynamic"
	default:
		switch complexType := configType.(type) {
		case *model.ObjectType:
			return typeName
		case *model.ListType:
			elementType := computeConfigTypeParam(configName, complexType.ElementType)
			return elementType + "[]"
		case *model.MapType:
			elementType := computeConfigTypeParam(configName, complexType.ElementType)
			return fmt.Sprintf("Dictionary<string, %s>", elementType)
		default:
			return "dynamic"
		}
	}
}

func (g *generator) genConfigVariable(w io.Writer, v *pcl.ConfigVariable) {
	if !g.configCreated {
		g.Fprintf(w, "%svar config = new Config();\n", g.Indent)
		g.configCreated = true
	}

	getType := "Object"
	switch pcl.UnwrapOption(v.Type()) {
	case model.StringType:
		getType = ""
	case model.NumberType:
		getType = "Double"
	case model.IntType:
		getType = "Int32"
	case model.BoolType:
		getType = "Boolean"
	}

	typeParam := ""
	if getType == "Object" {
		// compute the type parameter T for the call to config.GetObject<T>(...)
		computedTypeParam := computeConfigTypeParam(v.Name(), v.Type())
		typeParam = fmt.Sprintf("<%s>", computedTypeParam)
	}

	getOrRequire := "Get"
	if v.DefaultValue == nil {
		getOrRequire = "Require"
	}

	if v.Description != "" {
		for _, line := range strings.Split(v.Description, "\n") {
			g.Fgenf(w, "%s// %s\n", g.Indent, line)
		}
	}

	name := makeValidIdentifier(v.Name())
	if v.DefaultValue != nil && !model.IsOptionalType(v.Type()) {
		typ := v.DefaultValue.Type()
		if _, ok := typ.(*model.PromiseType); ok {
			g.Fgenf(w, "%svar %s = Output.Create(config.%s%s%s(\"%s\"))",
				g.Indent, name, getOrRequire, getType, typeParam, v.LogicalName())
		} else {
			g.Fgenf(w, "%svar %s = config.%s%s%s(\"%s\")",
				g.Indent, name, getOrRequire, getType, typeParam, v.LogicalName())
		}
		expr := g.lowerExpression(v.DefaultValue, v.DefaultValue.Type())
		g.Fgenf(w, " ?? %.v", expr)
	} else {
		g.Fgenf(w, "%svar %s = config.%s%s%s(\"%s\")",
			g.Indent, name, getOrRequire, getType, typeParam, v.LogicalName())
	}
	g.Fgenf(w, ";\n")
}

func (g *generator) genLocalVariable(w io.Writer, localVariable *pcl.LocalVariable) {
	g.genTrivia(w, localVariable.Definition.Tokens.Name)
	variableName := makeValidIdentifier(localVariable.Name())
	value := localVariable.Definition.Value
	functionSchema, isInvokeCall := g.isFunctionInvoke(localVariable)
	if isInvokeCall {
		result := g.lowerExpressionWithoutApplies(value, value.Type())
		g.functionInvokes[variableName] = functionSchema
		g.Fgenf(w, "%svar %s = %v;\n\n", g.Indent, variableName, result)
	} else {
		result := g.lowerExpression(value, value.Type())
		g.Fgenf(w, "%svar %s = %v;\n\n", g.Indent, variableName, result)
	}
}

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.DiagWarning,
		Summary:  message,
		Detail:   message,
	})
	g.Fgenf(w, "\"TODO: %s\"", fmt.Sprintf(reason, vs...))
}