// 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 nodejs

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

	"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/slice"
	"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
	"github.com/pulumi/pulumi/sdk/v3/go/common/workspace"
	"github.com/zclconf/go-cty/cty"
)

const PulumiToken = "pulumi"

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

	program     *pcl.Program
	diagnostics hcl.Diagnostics

	asyncMain     bool
	configCreated bool
	isComponent   bool
}

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

	g := &generator{
		program: program,
	}
	g.Formatter = format.NewFormatter(g)

	packages, err := program.PackageSnapshots()
	if err != nil {
		return nil, nil, err
	}
	for _, p := range packages {
		if err := p.ImportLanguages(map[string]schema.Language{"nodejs": Importer}); err != nil {
			return nil, nil, err
		}
	}

	var index bytes.Buffer
	err = g.genPreamble(&index, program)
	if err != nil {
		return nil, nil, err
	}
	// used to track declared variables in the main program
	// since outputs have identifiers which can conflict with other program nodes' identifiers
	// we switch the entry point to async which allows for declaring arbitrary output names
	declaredNodeIdentifiers := map[string]bool{}
	for _, n := range nodes {
		if g.asyncMain {
			break
		}
		switch x := n.(type) {
		case *pcl.Resource:
			if resourceRequiresAsyncMain(x) {
				g.asyncMain = true
			}
			declaredNodeIdentifiers[makeValidIdentifier(x.Name())] = true
		case *pcl.ConfigVariable:
			declaredNodeIdentifiers[makeValidIdentifier(x.Name())] = true
		case *pcl.LocalVariable:
			declaredNodeIdentifiers[makeValidIdentifier(x.Name())] = true
		case *pcl.Component:
			declaredNodeIdentifiers[makeValidIdentifier(x.Name())] = true
		case *pcl.OutputVariable:
			if outputRequiresAsyncMain(x) {
				g.asyncMain = true
			}

			outputIdentifier := makeValidIdentifier(x.Name())
			if _, alreadyDeclared := declaredNodeIdentifiers[outputIdentifier]; alreadyDeclared {
				g.asyncMain = true
			}
		}
	}

	indenter := func(f func()) { f() }
	if g.asyncMain {
		indenter = g.Indented
		g.Fgenf(&index, "export = async () => {\n")
	}

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

		if g.asyncMain {
			var result *model.ObjectConsExpression
			for _, n := range nodes {
				if o, ok := n.(*pcl.OutputVariable); ok {
					if result == nil {
						result = &model.ObjectConsExpression{}
					}
					name := o.LogicalName()
					result.Items = append(result.Items, model.ObjectConsItem{
						Key:   &model.LiteralValueExpression{Value: cty.StringVal(name)},
						Value: g.lowerExpression(o.Value, o.Type()),
					})
				}
			}
			if result != nil {
				g.Fgenf(&index, "%sreturn %v;\n", g.Indent, result)
			}
		}
	})

	if g.asyncMain {
		g.Fgenf(&index, "}\n")
	}

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

	for componentDir, component := range program.CollectComponents() {
		componentFilename := filepath.Base(componentDir)
		componentName := component.DeclarationName()
		componentGenerator := &generator{
			program:     component.Program,
			isComponent: true,
		}

		componentGenerator.Formatter = format.NewFormatter(componentGenerator)

		var componentBuffer bytes.Buffer
		componentGenerator.genComponentResourceDefinition(&componentBuffer, componentName, component)
		files[componentFilename+".ts"] = componentBuffer.Bytes()
	}

	return files, g.diagnostics, nil
}

func GenerateProject(
	directory string, project workspace.Project,
	program *pcl.Program, localDependencies map[string]string,
	forceTsc bool,
) 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 "nodejs" then marshal to Pulumi.yaml
	runtime := workspace.NewProjectRuntimeInfo("nodejs", nil)
	if forceTsc {
		runtime.SetOption("typescript", false)
	}
	project.Runtime = runtime

	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 the package.json
	var packageJSON bytes.Buffer
	fmt.Fprintf(&packageJSON, `{
	"name": "%s",
	"devDependencies": {
		"@types/node": "^14"
	},
	"dependencies": {
		"typescript": "^4.0.0",
		`, project.Name.String())

	// Check if pulumi is a local dependency, else add it as a normal range dependency
	if pulumiArtifact, has := localDependencies[PulumiToken]; has {
		fmt.Fprintf(&packageJSON, `"@pulumi/pulumi": "%s"`, pulumiArtifact)
	} else {
		fmt.Fprintf(&packageJSON, `"@pulumi/pulumi": "^3.0.0"`)
	}

	// For each package add a dependency line
	packages, err := program.CollectNestedPackageSnapshots()
	if err != nil {
		return err
	}
	// Sort the dependencies to ensure a deterministic package.json. Note that the typescript and
	// @pulumi/pulumi dependencies are already added above and not sorted.
	sortedPackageNames := make([]string, 0, len(packages))
	for k := range packages {
		sortedPackageNames = append(sortedPackageNames, k)
	}
	sort.Strings(sortedPackageNames)
	for _, k := range sortedPackageNames {
		p := packages[k]
		if p.Name == PulumiToken {
			continue
		}
		if err := p.ImportLanguages(map[string]schema.Language{"nodejs": Importer}); err != nil {
			return err
		}

		packageName := "@pulumi/" + p.Name
		err := p.ImportLanguages(map[string]schema.Language{"nodejs": Importer})
		if err != nil {
			return err
		}
		if langInfo, found := p.Language["nodejs"]; found {
			nodeInfo, ok := langInfo.(NodePackageInfo)
			if ok && nodeInfo.PackageName != "" {
				packageName = nodeInfo.PackageName
			}
		}

		dependencyTemplate := ",\n		\"%s\": \"%s\""
		if path, has := localDependencies[p.Name]; has {
			fmt.Fprintf(&packageJSON, dependencyTemplate, packageName, path)
		} else {
			if p.Version != nil {
				fmt.Fprintf(&packageJSON, dependencyTemplate, packageName, p.Version.String())
			} else {
				fmt.Fprintf(&packageJSON, dependencyTemplate, packageName, "*")
			}
		}
	}
	packageJSON.WriteString(`
	}
}`)

	files["package.json"] = packageJSON.Bytes()

	// Add the language specific .gitignore
	files[".gitignore"] = []byte(`/bin/
/node_modules/`)

	// Add the basic tsconfig
	var tsConfig bytes.Buffer
	tsConfig.WriteString(`{
	"compilerOptions": {
		"strict": true,
		"outDir": "bin",
		"target": "es2016",
		"module": "commonjs",
		"moduleResolution": "node",
		"sourceMap": true,
		"experimentalDecorators": true,
		"pretty": true,
		"noFallthroughCasesInSwitch": true,
		"noImplicitReturns": true,
		"forceConsistentCasingInFileNames": true
	},
	"files": [
`)

	fileNames := make([]string, 0, len(files))
	for file := range files {
		fileNames = append(fileNames, file)
	}
	sort.Strings(fileNames)

	for i, file := range fileNames {
		if strings.HasSuffix(file, ".ts") {
			tsConfig.WriteString("		\"" + file + "\"")
			lastFile := i == len(files)-1
			if !lastFile {
				tsConfig.WriteString(",\n")
			} else {
				tsConfig.WriteString("\n")
			}
		}
	}

	tsConfig.WriteString(`	]
}`)
	files["tsconfig.json"] = tsConfig.Bytes()

	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
}

// genLeadingTrivia generates the list of leading trivia assicated 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 assicated 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 assicated 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)
	}
}

type programImports struct {
	importStatements      []string
	preambleHelperMethods codegen.StringSet
}

func (g *generator) collectProgramImports(program *pcl.Program) programImports {
	importSet := codegen.NewStringSet("@pulumi/pulumi")
	preambleHelperMethods := codegen.NewStringSet()
	var componentImports []string

	npmToPuPkgName := make(map[string]string)
	seenComponentImports := map[string]bool{}
	for _, n := range program.Nodes {
		switch n := n.(type) {
		case *pcl.Resource:
			pkg, _, _, _ := n.DecomposeToken()
			if pkg == PulumiToken {
				continue
			}
			pkgName := "@pulumi/" + pkg
			if n.Schema != nil && n.Schema.PackageReference != nil {
				def, err := n.Schema.PackageReference.Definition()
				contract.AssertNoErrorf(err, "Should be able to retrieve definition for %s", n.Schema.Token)
				if info, ok := def.Language["nodejs"].(NodePackageInfo); ok && info.PackageName != "" {
					pkgName = info.PackageName
				}
				npmToPuPkgName[pkgName] = pkg
			}
			importSet.Add(pkgName)
		case *pcl.Component:
			componentDir := filepath.Base(n.DirPath())
			componentName := n.DeclarationName()
			dirAndName := componentDir + "-" + componentName
			if _, ok := seenComponentImports[dirAndName]; !ok {
				importStatement := fmt.Sprintf("import { %s } from \"./%s\";", componentName, componentDir)
				componentImports = append(componentImports, importStatement)
				seenComponentImports[dirAndName] = true
			}
		}
		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 {
						importSet.Add(importPackage)
					}
				}
				if helperMethodBody, ok := getHelperMethodIfNeeded(call.Name, g.Indent); ok {
					preambleHelperMethods.Add(helperMethodBody)
				}
			}
			return n, nil
		})
		contract.Assertf(len(diags) == 0, "unexpected diagnostics: %v", diags)
	}

	sortedValues := importSet.SortedValues()
	imports := slice.Prealloc[string](len(sortedValues))
	for _, pkg := range sortedValues {
		if pkg == "@pulumi/pulumi" {
			continue
		}
		var as string
		if puPkg, ok := npmToPuPkgName[pkg]; ok {
			as = makeValidIdentifier(puPkg)
		} else {
			as = makeValidIdentifier(path.Base(pkg))
		}
		imports = append(imports, fmt.Sprintf("import * as %v from \"%v\";", as, pkg))
	}

	imports = append(imports, componentImports...)
	sort.Strings(imports)

	return programImports{
		importStatements:      imports,
		preambleHelperMethods: preambleHelperMethods,
	}
}

func (g *generator) genPreamble(w io.Writer, program *pcl.Program) error {
	// Print the @pulumi/pulumi import at the top.
	g.Fprintln(w, `import * as pulumi from "@pulumi/pulumi";`)

	programImports := g.collectProgramImports(program)

	// Now sort the imports and emit them.
	for _, i := range programImports.importStatements {
		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 programImports.preambleHelperMethods.SortedValues() {
		g.Fprintf(w, "%s\n\n", preambleHelperMethodBody)
	}
	return nil
}

func componentElementType(pclType model.Type) string {
	switch pclType {
	case model.BoolType:
		return "boolean"
	case model.IntType, model.NumberType:
		return "number"
	case model.StringType:
		return "string"
	default:
		switch pclType := pclType.(type) {
		case *model.ListType:
			elementType := componentElementType(pclType.ElementType)
			return elementType + "[]"
		case *model.MapType:
			elementType := componentElementType(pclType.ElementType)
			return fmt.Sprintf("Record<string, pulumi.Input<%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 componentElementType(pclType.ElementType)
		case *model.UnionType:
			if len(pclType.ElementTypes) == 2 && pclType.ElementTypes[0] == model.NoneType {
				return componentElementType(pclType.ElementTypes[1])
			} else if len(pclType.ElementTypes) == 2 && pclType.ElementTypes[1] == model.NoneType {
				return componentElementType(pclType.ElementTypes[0])
			} else {
				return "any"
			}
		default:
			return "any"
		}
	}
}

func componentInputType(pclType model.Type) string {
	elementType := componentElementType(pclType)
	return fmt.Sprintf("pulumi.Input<%s>", elementType)
}

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

func (g *generator) genObjectTypedConfig(w io.Writer, objectType *model.ObjectType) {
	attributeKeys := []string{}
	for attributeKey := range objectType.Properties {
		attributeKeys = append(attributeKeys, attributeKey)
	}

	// get deterministically sorted keys
	sort.Strings(attributeKeys)

	g.Fgenf(w, "{\n")
	g.Indented(func() {
		for _, attributeKey := range attributeKeys {
			attributeType := objectType.Properties[attributeKey]
			optional := "?"
			g.Fgenf(w, "%s", g.Indent)
			typeName := componentInputType(attributeType)
			g.Fgenf(w, "%s%s: %s,\n", attributeKey, optional, typeName)
		}
	})
	g.Fgenf(w, "%s}", g.Indent)
}

func (g *generator) genComponentResourceDefinition(w io.Writer, componentName string, component *pcl.Component) {
	// Print the @pulumi/pulumi import at the top.
	g.Fprintln(w, `import * as pulumi from "@pulumi/pulumi";`)

	programImports := g.collectProgramImports(component.Program)

	// Now sort the imports and emit them.
	for _, i := range programImports.importStatements {
		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 programImports.preambleHelperMethods.SortedValues() {
		g.Fprintf(w, "%s\n\n", preambleHelperMethodBody)
	}

	configVars := component.Program.ConfigVariables()

	if len(configVars) > 0 {
		g.Fgenf(w, "interface %sArgs {\n", componentName)
		g.Indented(func() {
			for _, configVar := range configVars {
				optional := "?"
				if configVar.DefaultValue == nil {
					optional = ""
				}
				if configVar.Description != "" {
					g.Fgenf(w, "%s/**\n", g.Indent)
					for _, line := range strings.Split(configVar.Description, "\n") {
						g.Fgenf(w, "%s * %s\n", g.Indent, line)
					}
					g.Fgenf(w, "%s */\n", g.Indent)
				}

				g.Fgenf(w, "%s", g.Indent)
				switch configVarType := configVar.Type().(type) {
				case *model.ObjectType:
					// generate {...}
					g.Fgenf(w, "%s%s: ", configVar.Name(), optional)
					g.genObjectTypedConfig(w, configVarType)
					g.Fgen(w, ",\n")
				case *model.ListType:
					switch elementType := configVarType.ElementType.(type) {
					case *model.ObjectType:
						// generate {...}[]
						g.Fgenf(w, "%s%s: ", configVar.Name(), optional)
						g.genObjectTypedConfig(w, elementType)
						g.Fgen(w, "[],\n")
					default:
						typeName := componentInputType(configVar.Type())
						g.Fgenf(w, "%s%s: %s,\n", configVar.Name(), optional, typeName)
					}
				case *model.MapType:
					switch elementType := configVarType.ElementType.(type) {
					case *model.ObjectType:
						// generate Record<string, {...}>
						g.Fgenf(w, "%s%s: Record<string, ", configVar.Name(), optional)
						g.genObjectTypedConfig(w, elementType)
						g.Fgen(w, ">,\n")
					default:
						typeName := componentInputType(configVar.Type())
						g.Fgenf(w, "%s%s: %s,\n", configVar.Name(), optional, typeName)
					}
				default:
					typeName := componentInputType(configVar.Type())
					g.Fgenf(w, "%s%s: %s,\n", configVar.Name(), optional, typeName)
				}
			}
		})
		g.Fgenf(w, "}\n\n")
	}

	outputs := component.Program.OutputVariables()

	g.Fgenf(w, "export class %s extends pulumi.ComponentResource {\n", componentName)
	g.Indented(func() {
		for _, output := range outputs {
			var outputType string
			switch expr := output.Value.(type) {
			case *model.ScopeTraversalExpression:
				resource, ok := expr.Parts[0].(*pcl.Resource)
				if ok && len(expr.Parts) == 1 {
					pkg, module, memberName, diagnostics := resourceTypeName(resource)
					g.diagnostics = append(g.diagnostics, diagnostics...)

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

					qualifiedMemberName := fmt.Sprintf("%s%s.%s", pkg, module, memberName)
					// special case: the output is a Resource type
					outputType = fmt.Sprintf("pulumi.Output<%s>", qualifiedMemberName)
				} else {
					outputType = componentOutputType(expr.Type())
				}
			default:
				outputType = componentOutputType(expr.Type())
			}
			g.Fgenf(w, "%s", g.Indent)
			g.Fgenf(w, "public %s: %s;\n", output.Name(), outputType)
		}

		token := "components:index:" + componentName

		if len(configVars) == 0 {
			g.Fgenf(w, "%s", g.Indent)
			g.Fgen(w, "constructor(name: string, opts?: pulumi.ComponentResourceOptions) {\n")
			g.Indented(func() {
				g.Fgenf(w, "%s", g.Indent)
				g.Fgenf(w, "super(\"%s\", name, {}, opts);\n", token)
			})
		} else {
			g.Fgenf(w, "%s", g.Indent)
			argsTypeName := componentName + "Args"
			g.Fgenf(w, "constructor(name: string, args: %s, opts?: pulumi.ComponentResourceOptions) {\n",
				argsTypeName)
			g.Indented(func() {
				g.Fgenf(w, "%s", g.Indent)
				g.Fgenf(w, "super(\"%s\", name, args, opts);\n", token)
			})
		}

		// generate component resources and local variables
		g.Indented(func() {
			// assign default values to config inputs
			for _, configVar := range configVars {
				if configVar.DefaultValue != nil {
					g.Fgenf(w, "%sargs.%s = args.%s || %v;\n",
						g.Indent,
						configVar.Name(),
						configVar.Name(),
						configVar.DefaultValue)
				}
			}

			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:
					if node.Options == nil {
						node.Options = &pcl.ResourceOptions{}
					}

					if node.Options.Parent == nil {
						node.Options.Parent = model.ConstantReference(&model.Constant{
							Name: "this",
						})
					}
					g.genComponent(w, node)
					g.Fgen(w, "\n")
				case *pcl.Resource:
					if node.Options == nil {
						node.Options = &pcl.ResourceOptions{}
					}

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

			registeredOutputs := &model.ObjectConsExpression{}
			for _, output := range outputs {
				// assign the output fields
				outputProperty := 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 = pulumi.output(%v);\n",
							g.Indent, outputProperty,
							g.lowerExpression(output.Value, output.Type()))
					} else {
						g.Fgenf(w, "%sthis.%s = %v;\n",
							g.Indent, outputProperty,
							g.lowerExpression(output.Value, output.Type()))
					}
				default:
					g.Fgenf(w, "%sthis.%s = %v;\n",
						g.Indent, outputProperty,
						g.lowerExpression(output.Value, output.Type()))
				}
				// add the outputs to abject for registration
				registeredOutputs.Items = append(registeredOutputs.Items, model.ObjectConsItem{
					Key: &model.LiteralValueExpression{
						Tokens: syntax.NewLiteralValueTokens(cty.StringVal(output.Name())),
						Value:  cty.StringVal(output.Name()),
					},
					Value: output.Value,
				})
			}

			if len(outputs) == 0 {
				g.Fgenf(w, "%sthis.registerOutputs();\n", g.Indent)
			} else {
				g.Fgenf(w, "%sthis.registerOutputs(%v);\n", g.Indent, registeredOutputs)
			}
		})

		g.Fgenf(w, "%s}\n", g.Indent)
	})
	g.Fgen(w, "}\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.OutputVariable:
		g.genOutputVariable(w, n)
	case *pcl.Component:
		g.genComponent(w, n)
	}
}

func resourceRequiresAsyncMain(r *pcl.Resource) bool {
	if r.Options == nil || r.Options.Range == nil {
		return false
	}

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

func outputRequiresAsyncMain(ov *pcl.OutputVariable) bool {
	outputName := ov.LogicalName()
	return makeValidIdentifier(outputName) != outputName
}

// resourceTypeName computes the NodeJS package, module, and type name for the given resource.
func resourceTypeName(r *pcl.Resource) (string, string, string, hcl.Diagnostics) {
	// Compute the resource type from the Pulumi type token.
	pcl.FixupPulumiPackageTokens(r)
	pkg, module, member, diagnostics := r.DecomposeToken()

	if r.Schema != nil {
		module = moduleName(module, r.Schema.PackageReference)
	}

	return makeValidIdentifier(pkg), module, title(member), diagnostics
}

func moduleName(module string, pkg schema.PackageReference) string {
	// Normalize module.
	if pkg != nil {
		def, err := pkg.Definition()
		contract.AssertNoErrorf(err, "error loading package definition for %q", pkg.Name())
		err = def.ImportLanguages(map[string]schema.Language{"nodejs": Importer})
		contract.AssertNoErrorf(err, "error importing nodejs language for %q", pkg.Name())
		if lang, ok := def.Language["nodejs"]; ok {
			pkgInfo := lang.(NodePackageInfo)
			if m, ok := pkgInfo.ModuleToPackage[module]; ok {
				module = m
			}
		}
	}
	return strings.ToLower(strings.ReplaceAll(module, "/", "."))
}

// 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) string {
	if opts == nil {
		return ""
	}

	// Turn the resource options into an ObjectConsExpression and generate it.
	var object *model.ObjectConsExpression
	appendOption := func(name string, value model.Expression) {
		if object == nil {
			object = &model.ObjectConsExpression{}
		}
		object.Items = append(object.Items, model.ObjectConsItem{
			Key: &model.LiteralValueExpression{
				Tokens: syntax.NewLiteralValueTokens(cty.StringVal(name)),
				Value:  cty.StringVal(name),
			},
			Value: value,
		})
	}

	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 object == nil {
		return ""
	}

	var buffer bytes.Buffer
	g.Fgenf(&buffer, ", %v", g.lowerExpression(object, nil))
	return buffer.String()
}

// genResourceDeclaration handles the generation of instantiations of resources.
func (g *generator) genResourceDeclaration(w io.Writer, r *pcl.Resource, needsDefinition bool) {
	pkg, module, memberName, diagnostics := resourceTypeName(r)
	g.diagnostics = append(g.diagnostics, diagnostics...)

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

	qualifiedMemberName := fmt.Sprintf("%s%s.%s", pkg, module, memberName)

	optionsBag := g.genResourceOptions(r.Options)

	name := r.LogicalName()
	variableName := makeValidIdentifier(r.Name())

	if needsDefinition {
		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) {
		g.Fgenf(w, "new %s(%s, {", qualifiedMemberName, resName)
		indenter := func(f func()) { f() }
		if len(r.Inputs) > 1 {
			indenter = g.Indented
		}
		indenter(func() {
			fmtString := "%s: %.v"
			if len(r.Inputs) > 1 {
				fmtString = "\n" + g.Indent + "%s: %.v,"
			}

			for _, attr := range r.Inputs {
				propertyName := attr.Name
				if !isLegalIdentifier(propertyName) {
					propertyName = fmt.Sprintf("%q", propertyName)
				}

				if r.Schema != nil {
					destType, diagnostics := r.InputType.Traverse(hcl.TraverseAttr{Name: attr.Name})
					g.diagnostics = append(g.diagnostics, diagnostics...)
					g.Fgenf(w, fmtString, propertyName,
						g.lowerExpression(attr.Value, destType.(model.Type)))
				} else {
					g.Fgenf(w, fmtString, propertyName, attr.Value)
				}
			}
		})
		if len(r.Inputs) > 1 {
			g.Fgenf(w, "\n%s", g.Indent)
		}
		g.Fgenf(w, "}%s)", optionsBag)
	}

	if r.Options != nil && r.Options.Range != nil {
		rangeType := r.Options.Range.Type()
		rangeExpr := r.Options.Range
		if model.ContainsOutputs(r.Options.Range.Type()) {
			rangeExpr = g.lowerExpression(rangeExpr, rangeType)
			if model.InputType(model.BoolType).ConversionFrom(rangeType) == model.SafeConversion {
				g.Fgenf(w, "%slet %s: %s | undefined;\n", g.Indent, variableName, qualifiedMemberName)
			} else {
				g.Fgenf(w, "%sconst %s: %s[] = [];\n", g.Indent, variableName, qualifiedMemberName)
			}

			switch expr := rangeExpr.(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, "%.20v.apply(", applyArgs[0])
					} else {
						// Otherwise, generate a call to `pulumi.all([]).apply()`.
						g.Fgen(w, "pulumi.all([")
						for i, o := range applyArgs {
							if i > 0 {
								g.Fgen(w, ", ")
							}
							g.Fgenf(w, "%v", o)
						}
						g.Fgen(w, "]).apply(")
					}

					// Step 2: apply lambda function arguments
					switch len(applyLambda.Signature.Parameters) {
					case 0:
						g.Fgen(w, "()")
					case 1:
						g.Fgenf(w, "%s", applyLambda.Signature.Parameters[0].Name)
					default:
						g.Fgen(w, "([")
						for i, p := range applyLambda.Signature.Parameters {
							if i > 0 {
								g.Fgen(w, ", ")
							}
							g.Fgenf(w, "%s", p.Name)
						}
						g.Fgen(w, "])")
					}

					// 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
					g.Fgen(w, " => {\n")
					g.Indented(func() {
						r.Options.Range = applyLambda.Body
						g.genResourceDeclaration(w, r, false)
					})
					g.Fgenf(w, "%s});\n", g.Indent)
					return
				}

				// If we have anything else that returns output, just generate a normal `.apply`
				g.Fgenf(w, "%.20v.apply(rangeBody => {\n", rangeExpr)
				g.Indented(func() {
					r.Options.Range = model.VariableReference(&model.Variable{
						Name:         "rangeBody",
						VariableType: model.ResolveOutputs(rangeExpr.Type()),
					})
					g.genResourceDeclaration(w, r, false)
				})
				g.Fgenf(w, "%s});\n", g.Indent)
				return
			case *model.TupleConsExpression, *model.ForExpression:
				// A list or list generator 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.Fgenf(w, "pulumi.all(%.20v).apply(rangeBody => {\n", rangeExpr)
				g.Indented(func() {
					r.Options.Range = model.VariableReference(&model.Variable{
						Name:         "rangeBody",
						VariableType: model.ResolveOutputs(rangeExpr.Type()),
					})
					g.genResourceDeclaration(w, r, false)
				})
				g.Fgenf(w, "%s});\n", g.Indent)
				return

			default:
				// If we have anything else that returns output, just generate a normal `.apply`
				g.Fgenf(w, "%.20v.apply(rangeBody => {\n", rangeExpr)
				g.Indented(func() {
					r.Options.Range = model.VariableReference(&model.Variable{
						Name:         "rangeBody",
						VariableType: model.ResolveOutputs(rangeExpr.Type()),
					})
					g.genResourceDeclaration(w, r, false)
				})
				g.Fgenf(w, "%s});\n", g.Indent)
				return
			}
		}
		if model.InputType(model.BoolType).ConversionFrom(rangeType) == model.SafeConversion {
			if needsDefinition {
				g.Fgenf(w, "%slet %s: %s | undefined;\n", g.Indent, variableName, qualifiedMemberName)
			}

			g.Fgenf(w, "%sif (%.v) {\n", g.Indent, rangeExpr)
			g.Indented(func() {
				g.Fgenf(w, "%s%s = ", g.Indent, variableName)
				instantiate(g.makeResourceName(name, ""))
				g.Fgenf(w, ";\n")
			})
			g.Fgenf(w, "%s}\n", g.Indent)
		} else {
			if needsDefinition {
				g.Fgenf(w, "%sconst %s: %s[] = [];\n", g.Indent, variableName, qualifiedMemberName)
			}
			resKey := "key"
			if model.InputType(model.NumberType).ConversionFrom(rangeExpr.Type()) != model.NoConversion {
				g.Fgenf(w, "%sfor (const range = {value: 0}; range.value < %.12o; range.value++) {\n", g.Indent, rangeExpr)
				resKey = "value"
			} else {
				rangeExpr := &model.FunctionCallExpression{
					Name: "entries",
					Args: []model.Expression{rangeExpr},
				}
				g.Fgenf(w, "%sfor (const range of %.v) {\n", g.Indent, rangeExpr)
			}

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

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

func (g *generator) genResource(w io.Writer, r *pcl.Resource) {
	g.genResourceDeclaration(w, r, true)
}

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

	optionsBag := g.genResourceOptions(component.Options)

	name := component.LogicalName()
	variableName := makeValidIdentifier(component.Name())

	g.genTrivia(w, component.Definition.Tokens.GetType(""))
	for _, l := range component.Definition.Tokens.GetLabels(nil) {
		g.genTrivia(w, l)
	}
	g.genTrivia(w, component.Definition.Tokens.GetOpenBrace())
	configVars := component.Program.ConfigVariables()
	instantiate := func(resName string) {
		if len(configVars) == 0 {
			g.Fgenf(w, "new %s(%s%s)", componentName, resName, optionsBag)
			return
		}
		g.Fgenf(w, "new %s(%s, {", componentName, resName)
		indenter := func(f func()) { f() }
		if len(component.Inputs) > 1 {
			indenter = g.Indented
		}
		indenter(func() {
			fmtString := "%s: %.v"
			if len(component.Inputs) > 1 {
				fmtString = "\n" + g.Indent + "%s: %.v,"
			}

			for _, attr := range component.Inputs {
				propertyName := attr.Name
				if !isLegalIdentifier(propertyName) {
					propertyName = fmt.Sprintf("%q", propertyName)
				}

				g.Fgenf(w, fmtString, propertyName,
					g.lowerExpression(attr.Value, attr.Value.Type()))
			}
		})
		if len(component.Inputs) > 1 {
			g.Fgenf(w, "\n%s", g.Indent)
		}
		g.Fgenf(w, "}%s)", optionsBag)
	}

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

		if model.InputType(model.BoolType).ConversionFrom(rangeType) == model.SafeConversion {
			g.Fgenf(w, "%slet %s: %s | undefined;\n", g.Indent, variableName, componentName)
			g.Fgenf(w, "%sif (%.v) {\n", g.Indent, rangeExpr)
			g.Indented(func() {
				g.Fgenf(w, "%s%s = ", g.Indent, variableName)
				instantiate(g.makeResourceName(name, ""))
				g.Fgenf(w, ";\n")
			})
			g.Fgenf(w, "%s}\n", g.Indent)
		} else {
			g.Fgenf(w, "%sconst %s: %s[] = [];\n", g.Indent, variableName, componentName)

			resKey := "key"
			if model.InputType(model.NumberType).ConversionFrom(rangeExpr.Type()) != model.NoConversion {
				g.Fgenf(w, "%sfor (const range = {value: 0}; range.value < %.12o; range.value++) {\n", g.Indent, rangeExpr)
				resKey = "value"
			} else {
				rangeExpr := &model.FunctionCallExpression{
					Name: "entries",
					Args: []model.Expression{rangeExpr},
				}
				g.Fgenf(w, "%sfor (const range of %.v) {\n", g.Indent, rangeExpr)
			}

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

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

func computeConfigTypeParam(configType model.Type) string {
	switch pcl.UnwrapOption(configType) {
	case model.StringType:
		return "string"
	case model.NumberType, model.IntType:
		return "number"
	case model.BoolType:
		return "boolean"
	case model.DynamicType:
		return "any"
	default:
		switch complexType := pcl.UnwrapOption(configType).(type) {
		case *model.ListType:
			return fmt.Sprintf("Array<%s>", computeConfigTypeParam(complexType.ElementType))
		case *model.MapType:
			return fmt.Sprintf("Record<string, %s>", computeConfigTypeParam(complexType.ElementType))
		case *model.ObjectType:
			if len(complexType.Properties) == 0 {
				return "any"
			}

			attributeKeys := []string{}
			for attributeKey := range complexType.Properties {
				attributeKeys = append(attributeKeys, attributeKey)
			}
			// get deterministically sorted attribute keys
			sort.Strings(attributeKeys)

			var elementTypes []string
			for _, propertyName := range attributeKeys {
				propertyType := complexType.Properties[propertyName]
				elementType := fmt.Sprintf("%s?: %s", propertyName, computeConfigTypeParam(propertyType))
				elementTypes = append(elementTypes, elementType)
			}

			return fmt.Sprintf("{%s}", strings.Join(elementTypes, ", "))
		default:
			return "any"
		}
	}
}

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

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

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

	getOrRequire := "get"
	if v.DefaultValue == nil && !model.IsOptionalType(v.Type()) {
		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())
	g.Fgenf(w, "%[1]sconst %[2]s = config.%[3]s%[4]s%[5]s(\"%[6]s\")",
		g.Indent, name, getOrRequire, getType, typeParam, v.LogicalName())
	if v.DefaultValue != nil && !model.IsOptionalType(v.Type()) {
		g.Fgenf(w, " || %.v", g.lowerExpression(v.DefaultValue, v.DefaultValue.Type()))
	}
	g.Fgenf(w, ";\n")
}

func (g *generator) genLocalVariable(w io.Writer, v *pcl.LocalVariable) {
	g.genTrivia(w, v.Definition.Tokens.Name)
	g.Fgenf(w, "%sconst %s = %.3v;\n", g.Indent, v.Name(), g.lowerExpression(v.Definition.Value, v.Type()))
}

func (g *generator) genOutputVariable(w io.Writer, v *pcl.OutputVariable) {
	if g.asyncMain {
		// skip generating the output variables as export constants
		// when we are inside an async main program because we export them as a single object
		return
	}

	// TODO(pdg): trivia
	g.Fgenf(w, "%sexport const %s = %.3v;\n", g.Indent,
		makeValidIdentifier(v.Name()), g.lowerExpression(v.Value, v.Type()))
}

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, "(() => throw new Error(%q))()", fmt.Sprintf(reason, vs...))
}