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

import (
	"bytes"
	_ "embed"
	"encoding/json"
	"fmt"
	"io"
	"math"
	"net/url"
	"os"
	"path"
	"regexp"
	"sort"
	"strings"

	"github.com/blang/semver"
	"github.com/hashicorp/hcl/v2"
	"github.com/pkg/errors"
	"github.com/santhosh-tekuri/jsonschema/v5"
	"gopkg.in/yaml.v3"

	"github.com/pulumi/pulumi/sdk/v3/go/common/resource/plugin"
	"github.com/pulumi/pulumi/sdk/v3/go/common/util/contract"
)

// TODO:
// - Providerless packages

// Type represents a datatype in the Pulumi Schema. Types created by this package are identical if they are
// equal values.
type Type interface {
	String() string

	isType()
}

type primitiveType int

const (
	boolType    primitiveType = 1
	intType     primitiveType = 2
	numberType  primitiveType = 3
	stringType  primitiveType = 4
	archiveType primitiveType = 5
	assetType   primitiveType = 6
	anyType     primitiveType = 7
	jsonType    primitiveType = 8
)

//nolint: goconst
func (t primitiveType) String() string {
	switch t {
	case boolType:
		return "boolean"
	case intType:
		return "integer"
	case numberType:
		return "number"
	case stringType:
		return "string"
	case archiveType:
		return "pulumi:pulumi:Archive"
	case assetType:
		return "pulumi:pulumi:Asset"
	case jsonType:
		fallthrough
	case anyType:
		return "pulumi:pulumi:Any"
	default:
		panic("unknown primitive type")
	}
}

func (primitiveType) isType() {}

// IsPrimitiveType returns true if the given Type is a primitive type. The primitive types are bool, int, number,
// string, archive, asset, and any.
func IsPrimitiveType(t Type) bool {
	_, ok := plainType(t).(primitiveType)
	return ok
}

var (
	// BoolType represents the set of boolean values.
	BoolType Type = boolType
	// IntType represents the set of 32-bit integer values.
	IntType Type = intType
	// NumberType represents the set of IEEE754 double-precision values.
	NumberType Type = numberType
	// StringType represents the set of UTF-8 string values.
	StringType Type = stringType
	// ArchiveType represents the set of Pulumi Archive values.
	ArchiveType Type = archiveType
	// AssetType represents the set of Pulumi Asset values.
	AssetType Type = assetType
	// JSONType represents the set of JSON-encoded values.
	JSONType Type = jsonType
	// AnyType represents the complete set of values.
	AnyType Type = anyType
)

// An InvalidType represents an invalid type with associated diagnostics.
type InvalidType struct {
	Diagnostics hcl.Diagnostics
}

func (t *InvalidType) String() string {
	return "Invalid"
}

func (*InvalidType) isType() {}

func invalidType(diags ...*hcl.Diagnostic) (Type, hcl.Diagnostics) {
	t := &InvalidType{Diagnostics: hcl.Diagnostics(diags)}
	return t, hcl.Diagnostics(diags)
}

// MapType represents maps from strings to particular element types.
type MapType struct {
	// ElementType is the element type of the map.
	ElementType Type
}

func (t *MapType) String() string {
	return fmt.Sprintf("Map<%v>", t.ElementType)
}

func (*MapType) isType() {}

// ArrayType represents arrays of particular element types.
type ArrayType struct {
	// ElementType is the element type of the array.
	ElementType Type
}

func (t *ArrayType) String() string {
	return fmt.Sprintf("Array<%v>", t.ElementType)
}

func (*ArrayType) isType() {}

// EnumType represents an enum.
type EnumType struct {
	// Package is the type's package.
	Package *Package
	// Token is the type's Pulumi type token.
	Token string
	// Comment is the description of the type, if any.
	Comment string
	// Elements are the predefined enum values.
	Elements []*Enum
	// ElementType is the underlying type for the enum.
	ElementType Type
}

// Enum contains information about an enum.
type Enum struct {
	// Value is the value of the enum.
	Value interface{}
	// Comment is the description for the enum value.
	Comment string
	// Name for the enum.
	Name string
	// DeprecationMessage indicates whether or not the value is deprecated.
	DeprecationMessage string
}

func (t *EnumType) String() string {
	return t.Token
}

func (*EnumType) isType() {}

// UnionType represents values that may be any one of a specified set of types.
type UnionType struct {
	// ElementTypes are the allowable types for the union type.
	ElementTypes []Type
	// DefaultType is the default type, if any, for the union type. This can be used by targets that do not support
	// unions, or in positions where unions are not appropriate.
	DefaultType Type
	// Discriminator informs the consumer of an alternative schema based on the value associated with it.
	Discriminator string
	// Mapping is an optional object to hold mappings between payload values and schema names or references.
	Mapping map[string]string
}

func (t *UnionType) String() string {
	elements := make([]string, len(t.ElementTypes)+1)
	for i, e := range t.ElementTypes {
		elements[i] = e.String()
	}

	def := "default="
	if t.DefaultType != nil {
		def += t.DefaultType.String()
	}
	elements[len(elements)-1] = def

	return fmt.Sprintf("Union<%v>", strings.Join(elements, ", "))
}

func (*UnionType) isType() {}

// ObjectType represents schematized maps from strings to particular types.
type ObjectType struct {
	// Package is the package that defines the resource.
	Package *Package
	// Token is the type's Pulumi type token.
	Token string
	// Comment is the description of the type, if any.
	Comment string
	// Properties is the list of the type's properties.
	Properties []*Property
	// Language specifies additional language-specific data about the object type.
	Language map[string]interface{}

	// InputShape is the input shape for this object. Only valid if IsPlainShape returns true.
	InputShape *ObjectType
	// PlainShape is the plain shape for this object. Only valid if IsInputShape returns true.
	PlainShape *ObjectType

	properties map[string]*Property
}

// IsPlainShape returns true if this object type is the plain shape of a (plain, input)
// pair. The plain shape of an object does not contain *InputType values and only
// references other plain shapes.
func (t *ObjectType) IsPlainShape() bool {
	return t.PlainShape == nil
}

// IsInputShape returns true if this object type is the plain shape of a (plain, input)
// pair. The input shape of an object may contain *InputType values and may
// reference other input shapes.
func (t *ObjectType) IsInputShape() bool {
	return t.PlainShape != nil
}

func (t *ObjectType) Property(name string) (*Property, bool) {
	if t.properties == nil && len(t.Properties) > 0 {
		t.properties = make(map[string]*Property)
		for _, p := range t.Properties {
			t.properties[p.Name] = p
		}
	}
	p, ok := t.properties[name]
	return p, ok
}

func (t *ObjectType) String() string {
	if t.PlainShape != nil {
		return t.Token + "•Input"
	}
	return t.Token
}

func (*ObjectType) isType() {}

type ResourceType struct {
	// Token is the type's Pulumi type token.
	Token string
	// Resource is the type's underlying resource.
	Resource *Resource
}

func (t *ResourceType) String() string {
	return t.Token
}

func (t *ResourceType) isType() {}

// TokenType represents an opaque type that is referred to only by its token. A TokenType may have an underlying type
// that can be used in place of the token.
type TokenType struct {
	// Token is the type's Pulumi type token.
	Token string
	// Underlying type is the type's underlying type, if any.
	UnderlyingType Type
}

func (t *TokenType) String() string {
	return t.Token
}

func (*TokenType) isType() {}

// InputType represents a type that accepts either a prompt value or an output value.
type InputType struct {
	// ElementType is the element type of the input.
	ElementType Type
}

func (t *InputType) String() string {
	return fmt.Sprintf("Input<%v>", t.ElementType)
}

func (*InputType) isType() {}

// OptionalType represents a type that accepts an optional value.
type OptionalType struct {
	// ElementType is the element type of the input.
	ElementType Type
}

func (t *OptionalType) String() string {
	return fmt.Sprintf("Optional<%v>", t.ElementType)
}

func (*OptionalType) isType() {}

// DefaultValue describes a default value for a property.
type DefaultValue struct {
	// Value specifies a static default value, if any. This value must be representable in the Pulumi schema type
	// system, and its type must be assignable to that of the property to which the default applies.
	Value interface{}
	// Environment specifies a set of environment variables to probe for a default value.
	Environment []string
	// Language specifies additional language-specific data about the default value.
	Language map[string]interface{}
}

// Property describes an object or resource property.
type Property struct {
	// Name is the name of the property.
	Name string
	// Comment is the description of the property, if any.
	Comment string
	// Type is the type of the property.
	Type Type
	// ConstValue is the constant value for the property, if any.
	ConstValue interface{}
	// DefaultValue is the default value for the property, if any.
	DefaultValue *DefaultValue
	// DeprecationMessage indicates whether or not the property is deprecated.
	DeprecationMessage string
	// Language specifies additional language-specific data about the property.
	Language map[string]interface{}
	// Secret is true if the property is secret (default false).
	Secret bool
	// ReplaceOnChanges specifies if the property is to be replaced instead of updated (default false).
	ReplaceOnChanges bool
}

// IsRequired returns true if this property is required (i.e. its type is not Optional).
func (p *Property) IsRequired() bool {
	_, optional := p.Type.(*OptionalType)
	return !optional
}

// Alias describes an alias for a Pulumi resource.
type Alias struct {
	// Name is the "name" portion of the alias, if any.
	Name *string
	// Project is the "project" portion of the alias, if any.
	Project *string
	// Type is the "type" portion of the alias, if any.
	Type *string
}

// Resource describes a Pulumi resource.
type Resource struct {
	// Package is the package that defines the resource.
	Package *Package
	// Token is the resource's Pulumi type token.
	Token string
	// Comment is the description of the resource, if any.
	Comment string
	// IsProvider is true if the resource is a provider resource.
	IsProvider bool
	// InputProperties is the list of the resource's input properties.
	InputProperties []*Property
	// Properties is the list of the resource's output properties. This should be a superset of the input properties.
	Properties []*Property
	// StateInputs is the set of inputs used to get an existing resource, if any.
	StateInputs *ObjectType
	// Aliases is the list of aliases for the resource.
	Aliases []*Alias
	// DeprecationMessage indicates whether or not the resource is deprecated.
	DeprecationMessage string
	// Language specifies additional language-specific data about the resource.
	Language map[string]interface{}
	// IsComponent indicates whether the resource is a ComponentResource.
	IsComponent bool
	// Methods is the list of methods for the resource.
	Methods []*Method
}

// The set of resource paths where ReplaceOnChanges is true.
//
// For example, if you have the following resource struct:
//
// Resource A {
// Properties: {
// 	 Object B {
// 	   Object D: {
// 	     ReplaceOnChanges: true
// 	     }
// 	   Object F: {}
//     }
// 	 Object C {
// 	   ReplaceOnChanges: true
// 	   }
//   }
// }
//
// A.ReplaceOnChanges() == [[B, D], [C]]
func (r *Resource) ReplaceOnChanges() (changes [][]*Property, err []error) {
	for _, p := range r.Properties {
		if p.ReplaceOnChanges {
			changes = append(changes, []*Property{p})
		} else {
			stack := map[string]struct{}{p.Type.String(): {}}
			childChanges, errList := replaceOnChangesType(p.Type, &stack)
			err = append(err, errList...)

			for _, c := range childChanges {
				changes = append(changes, append([]*Property{p}, c...))
			}
		}
	}
	for i, e := range err {
		err[i] = errors.Wrapf(e, "Failed to genereate full `ReplaceOnChanges`")
	}
	return changes, err
}

func replaceOnChangesType(t Type, stack *map[string]struct{}) ([][]*Property, []error) {
	var errTmp []error
	if o, ok := t.(*OptionalType); ok {
		return replaceOnChangesType(o.ElementType, stack)
	} else if o, ok := t.(*ObjectType); ok {
		changes := [][]*Property{}
		err := []error{}
		for _, p := range o.Properties {
			if p.ReplaceOnChanges {
				changes = append(changes, []*Property{p})
			} else if _, ok := (*stack)[p.Type.String()]; !ok {
				// We handle recursive objects
				(*stack)[p.Type.String()] = struct{}{}
				var object [][]*Property
				object, errTmp = replaceOnChangesType(p.Type, stack)
				err = append(err, errTmp...)
				for _, path := range object {
					changes = append(changes, append([]*Property{p}, path...))
				}

				delete(*stack, p.Type.String())
			} else {
				err = append(err, errors.Errorf("Found recursive object %q", p.Name))
			}
		}
		// We don't want to emit errors where replaceOnChanges is not used.
		if len(changes) == 0 {
			return nil, nil
		}
		return changes, err
	} else if a, ok := t.(*ArrayType); ok {
		// This looks for types internal to the array, not a property of the array.
		return replaceOnChangesType(a.ElementType, stack)
	} else if m, ok := t.(*MapType); ok {
		// This looks for types internal to the map, not a property of the array.
		return replaceOnChangesType(m.ElementType, stack)
	}
	return nil, nil
}

// Joins the output of `ReplaceOnChanges` into property path names.
//
// For example, given an input [[B, D], [C]] where each property has a name
// equivalent to it's variable, this function should yield: ["B.D", "C"]
func PropertyListJoinToString(propertyList [][]*Property, nameConverter func(string) string) []string {
	var nonOptional func(Type) Type
	nonOptional = func(t Type) Type {
		if o, ok := t.(*OptionalType); ok {
			return nonOptional(o.ElementType)
		}
		return t
	}
	out := make([]string, len(propertyList))
	for i, p := range propertyList {
		names := make([]string, len(p))
		for j, n := range p {
			if _, ok := nonOptional(n.Type).(*ArrayType); ok {
				names[j] = nameConverter(n.Name) + "[*]"
			} else if _, ok := nonOptional(n.Type).(*MapType); ok {
				names[j] = nameConverter(n.Name) + ".*"
			} else {
				names[j] = nameConverter(n.Name)
			}
		}
		out[i] = strings.Join(names, ".")
	}
	return out
}

type Method struct {
	// Name is the name of the method.
	Name string
	// Function is the function definition for the method.
	Function *Function
}

// Function describes a Pulumi function.
type Function struct {
	// Package is the package that defines the function.
	Package *Package
	// Token is the function's Pulumi type token.
	Token string
	// Comment is the description of the function, if any.
	Comment string
	// Inputs is the bag of input values for the function, if any.
	Inputs *ObjectType
	// Outputs is the bag of output values for the function, if any.
	Outputs *ObjectType
	// DeprecationMessage indicates whether or not the function is deprecated.
	DeprecationMessage string
	// Language specifies additional language-specific data about the function.
	Language map[string]interface{}
	// IsMethod indicates whether the function is a method of a resource.
	IsMethod bool
}

// Package describes a Pulumi package.
type Package struct {
	moduleFormat *regexp.Regexp

	// Name is the unqualified name of the package (e.g. "aws", "azure", "gcp", "kubernetes". "random")
	Name string
	// Version is the version of the package.
	Version *semver.Version
	// Description is the description of the package.
	Description string
	// Keywords is the list of keywords that are associated with the package, if any.
	Keywords []string
	// Homepage is the package's homepage.
	Homepage string
	// License indicates which license is used for the package's contents.
	License string
	// Attribution allows freeform text attribution of derived work, if needed.
	Attribution string
	// Repository is the URL at which the source for the package can be found.
	Repository string
	// LogoURL is the URL for the package's logo, if any.
	LogoURL string
	// PluginDownloadURL is the URL to use to acquire the provider plugin binary, if any.
	PluginDownloadURL string

	// Types is the list of non-resource types defined by the package.
	Types []Type
	// Config is the set of configuration properties defined by the package.
	Config []*Property
	// Provider is the resource provider for the package, if any.
	Provider *Resource
	// Resources is the list of resource types defined by the package.
	Resources []*Resource
	// Functions is the list of functions defined by the package.
	Functions []*Function
	// Language specifies additional language-specific data about the package.
	Language map[string]interface{}

	resourceTable     map[string]*Resource
	resourceTypeTable map[string]*ResourceType
	functionTable     map[string]*Function
	typeTable         map[string]Type

	importedLanguages map[string]struct{}
}

// Language provides hooks for importing language-specific metadata in a package.
type Language interface {
	// ImportDefaultSpec decodes language-specific metadata associated with a DefaultValue.
	ImportDefaultSpec(def *DefaultValue, bytes json.RawMessage) (interface{}, error)
	// ImportPropertySpec decodes language-specific metadata associated with a Property.
	ImportPropertySpec(property *Property, bytes json.RawMessage) (interface{}, error)
	// ImportObjectTypeSpec decodes language-specific metadata associated with a ObjectType.
	ImportObjectTypeSpec(object *ObjectType, bytes json.RawMessage) (interface{}, error)
	// ImportResourceSpec decodes language-specific metadata associated with a Resource.
	ImportResourceSpec(resource *Resource, bytes json.RawMessage) (interface{}, error)
	// ImportFunctionSpec decodes language-specific metadata associated with a Function.
	ImportFunctionSpec(function *Function, bytes json.RawMessage) (interface{}, error)
	// ImportPackageSpec decodes language-specific metadata associated with a Package.
	ImportPackageSpec(pkg *Package, bytes json.RawMessage) (interface{}, error)
}

func sortedLanguageNames(metadata map[string]interface{}) []string {
	names := make([]string, 0, len(metadata))
	for lang := range metadata {
		names = append(names, lang)
	}
	sort.Strings(names)
	return names
}

func importDefaultLanguages(def *DefaultValue, languages map[string]Language) error {
	for _, name := range sortedLanguageNames(def.Language) {
		val := def.Language[name]
		if raw, ok := val.(json.RawMessage); ok {
			if lang, ok := languages[name]; ok {
				val, err := lang.ImportDefaultSpec(def, raw)
				if err != nil {
					return errors.Wrapf(err, "importing %v metadata", name)
				}
				def.Language[name] = val
			}
		}
	}
	return nil
}

func importPropertyLanguages(property *Property, languages map[string]Language) error {
	if property.DefaultValue != nil {
		if err := importDefaultLanguages(property.DefaultValue, languages); err != nil {
			return errors.Wrapf(err, "importing default value")
		}
	}

	for _, name := range sortedLanguageNames(property.Language) {
		val := property.Language[name]
		if raw, ok := val.(json.RawMessage); ok {
			if lang, ok := languages[name]; ok {
				val, err := lang.ImportPropertySpec(property, raw)
				if err != nil {
					return errors.Wrapf(err, "importing %v metadata", name)
				}
				property.Language[name] = val
			}
		}
	}
	return nil
}

func importObjectTypeLanguages(object *ObjectType, languages map[string]Language) error {
	for _, property := range object.Properties {
		if err := importPropertyLanguages(property, languages); err != nil {
			return errors.Wrapf(err, "importing property %v", property.Name)
		}
	}

	for _, name := range sortedLanguageNames(object.Language) {
		val := object.Language[name]
		if raw, ok := val.(json.RawMessage); ok {
			if lang, ok := languages[name]; ok {
				val, err := lang.ImportObjectTypeSpec(object, raw)
				if err != nil {
					return errors.Wrapf(err, "importing %v metadata", name)
				}
				object.Language[name] = val
			}
		}
	}
	return nil
}

func importResourceLanguages(resource *Resource, languages map[string]Language) error {
	for _, property := range resource.InputProperties {
		if err := importPropertyLanguages(property, languages); err != nil {
			return errors.Wrapf(err, "importing input property %v", property.Name)
		}
	}

	for _, property := range resource.Properties {
		if err := importPropertyLanguages(property, languages); err != nil {
			return errors.Wrapf(err, "importing property %v", property.Name)
		}
	}

	if resource.StateInputs != nil {
		for _, property := range resource.StateInputs.Properties {
			if err := importPropertyLanguages(property, languages); err != nil {
				return errors.Wrapf(err, "importing state input property %v", property.Name)
			}
		}
	}

	for _, name := range sortedLanguageNames(resource.Language) {
		val := resource.Language[name]
		if raw, ok := val.(json.RawMessage); ok {
			if lang, ok := languages[name]; ok {
				val, err := lang.ImportResourceSpec(resource, raw)
				if err != nil {
					return errors.Wrapf(err, "importing %v metadata", name)
				}
				resource.Language[name] = val
			}
		}
	}
	return nil
}

func importFunctionLanguages(function *Function, languages map[string]Language) error {
	if function.Inputs != nil {
		if err := importObjectTypeLanguages(function.Inputs, languages); err != nil {
			return errors.Wrapf(err, "importing inputs")
		}
	}
	if function.Outputs != nil {
		if err := importObjectTypeLanguages(function.Outputs, languages); err != nil {
			return errors.Wrapf(err, "importing outputs")
		}
	}

	for _, name := range sortedLanguageNames(function.Language) {
		val := function.Language[name]
		if raw, ok := val.(json.RawMessage); ok {
			if lang, ok := languages[name]; ok {
				val, err := lang.ImportFunctionSpec(function, raw)
				if err != nil {
					return errors.Wrapf(err, "importing %v metadata", name)
				}
				function.Language[name] = val
			}
		}
	}
	return nil
}

func (pkg *Package) ImportLanguages(languages map[string]Language) error {
	if pkg.importedLanguages == nil {
		pkg.importedLanguages = map[string]struct{}{}
	}

	any := false
	for lang := range languages {
		if _, ok := pkg.importedLanguages[lang]; !ok {
			any = true
			break
		}
	}
	if !any {
		return nil
	}

	for _, t := range pkg.Types {
		if object, ok := t.(*ObjectType); ok {
			if err := importObjectTypeLanguages(object, languages); err != nil {
				return errors.Wrapf(err, "importing object type %v", object.Token)
			}
		}
	}

	for _, config := range pkg.Config {
		if err := importPropertyLanguages(config, languages); err != nil {
			return errors.Wrapf(err, "importing configuration property %v", config.Name)
		}
	}

	if pkg.Provider != nil {
		if err := importResourceLanguages(pkg.Provider, languages); err != nil {
			return errors.Wrapf(err, "importing provider")
		}
	}

	for _, resource := range pkg.Resources {
		if err := importResourceLanguages(resource, languages); err != nil {
			return errors.Wrapf(err, "importing resource %v", resource.Token)
		}
	}

	for _, function := range pkg.Functions {
		if err := importFunctionLanguages(function, languages); err != nil {
			return errors.Wrapf(err, "importing function %v", function.Token)
		}
	}

	for _, name := range sortedLanguageNames(pkg.Language) {
		val := pkg.Language[name]
		if raw, ok := val.(json.RawMessage); ok {
			if lang, ok := languages[name]; ok {
				val, err := lang.ImportPackageSpec(pkg, raw)
				if err != nil {
					return errors.Wrapf(err, "importing %v metadata", name)
				}
				pkg.Language[name] = val
			}
		}
	}

	for lang := range languages {
		pkg.importedLanguages[lang] = struct{}{}
	}

	return nil
}

func (pkg *Package) TokenToModule(tok string) string {
	// token := pkg ":" module ":" member

	components := strings.Split(tok, ":")
	if len(components) != 3 {
		return ""
	}

	switch components[1] {
	case "providers":
		return ""
	default:
		if pkg.moduleFormat == nil {
			pkg.moduleFormat = defaultModuleFormat
		}

		matches := pkg.moduleFormat.FindStringSubmatch(components[1])
		if len(matches) < 2 || strings.HasPrefix(matches[1], "index") {
			return ""
		}
		return matches[1]
	}
}

func (pkg *Package) TokenToRuntimeModule(tok string) string {
	// token := pkg ":" module ":" member

	components := strings.Split(tok, ":")
	if len(components) != 3 {
		return ""
	}
	return components[1]
}

func (pkg *Package) GetResource(token string) (*Resource, bool) {
	r, ok := pkg.resourceTable[token]
	return r, ok
}

func (pkg *Package) GetFunction(token string) (*Function, bool) {
	f, ok := pkg.functionTable[token]
	return f, ok
}

func (pkg *Package) GetResourceType(token string) (*ResourceType, bool) {
	t, ok := pkg.resourceTypeTable[token]
	return t, ok
}

func (pkg *Package) GetType(token string) (Type, bool) {
	t, ok := pkg.typeTable[token]
	return t, ok
}

func (pkg *Package) MarshalSpec() (spec *PackageSpec, err error) {
	version := ""
	if pkg.Version != nil {
		version = pkg.Version.String()
	}

	var metadata *MetadataSpec
	if pkg.moduleFormat != nil {
		metadata = &MetadataSpec{ModuleFormat: pkg.moduleFormat.String()}
	}

	spec = &PackageSpec{
		Name:              pkg.Name,
		Version:           version,
		Description:       pkg.Description,
		Keywords:          pkg.Keywords,
		Homepage:          pkg.Homepage,
		License:           pkg.License,
		Attribution:       pkg.Attribution,
		Repository:        pkg.Repository,
		LogoURL:           pkg.LogoURL,
		PluginDownloadURL: pkg.PluginDownloadURL,
		Meta:              metadata,
		Types:             map[string]ComplexTypeSpec{},
		Resources:         map[string]ResourceSpec{},
		Functions:         map[string]FunctionSpec{},
	}

	spec.Config.Required, spec.Config.Variables, err = pkg.marshalProperties(pkg.Config, true)
	if err != nil {
		return nil, fmt.Errorf("marshaling package config: %w", err)
	}

	spec.Provider, err = pkg.marshalResource(pkg.Provider)
	if err != nil {
		return nil, fmt.Errorf("marshaling provider: %w", err)
	}

	for _, t := range pkg.Types {
		switch t := t.(type) {
		case *ObjectType:
			if t.IsInputShape() {
				continue
			}

			// Use the input shape when marshaling in order to get the plain annotations right.
			o, err := pkg.marshalObject(t.InputShape, false)
			if err != nil {
				return nil, fmt.Errorf("marshaling type '%v': %w", t.Token, err)
			}
			spec.Types[t.Token] = o
		case *EnumType:
			spec.Types[t.Token] = pkg.marshalEnum(t)
		}
	}

	for _, res := range pkg.Resources {
		r, err := pkg.marshalResource(res)
		if err != nil {
			return nil, fmt.Errorf("marshaling resource '%v': %w", res.Token, err)
		}
		spec.Resources[res.Token] = r
	}

	for _, fn := range pkg.Functions {
		f, err := pkg.marshalFunction(fn)
		if err != nil {
			return nil, fmt.Errorf("marshaling function '%v': %w", fn.Token, err)
		}
		spec.Functions[fn.Token] = f
	}

	return spec, nil
}

func (pkg *Package) MarshalJSON() ([]byte, error) {
	spec, err := pkg.MarshalSpec()
	if err != nil {
		return nil, err
	}
	return jsonMarshal(spec)
}

func (pkg *Package) MarshalYAML() ([]byte, error) {
	spec, err := pkg.MarshalSpec()
	if err != nil {
		return nil, err
	}

	var b bytes.Buffer
	enc := yaml.NewEncoder(&b)
	enc.SetIndent(2)
	if err := enc.Encode(spec); err != nil {
		return nil, err
	}
	return b.Bytes(), nil
}

func (pkg *Package) marshalObjectData(
	comment string, properties []*Property, language map[string]interface{}, plain bool) (ObjectTypeSpec, error) {

	required, props, err := pkg.marshalProperties(properties, plain)
	if err != nil {
		return ObjectTypeSpec{}, err
	}

	lang, err := marshalLanguage(language)
	if err != nil {
		return ObjectTypeSpec{}, err
	}

	return ObjectTypeSpec{
		Description: comment,
		Properties:  props,
		Type:        "object",
		Required:    required,
		Language:    lang,
	}, nil
}

func (pkg *Package) marshalObject(t *ObjectType, plain bool) (ComplexTypeSpec, error) {
	data, err := pkg.marshalObjectData(t.Comment, t.Properties, t.Language, plain)
	if err != nil {
		return ComplexTypeSpec{}, err
	}
	return ComplexTypeSpec{ObjectTypeSpec: data}, nil
}

func (pkg *Package) marshalEnum(t *EnumType) ComplexTypeSpec {
	values := make([]EnumValueSpec, len(t.Elements))
	for i, el := range t.Elements {
		values[i] = EnumValueSpec{
			Name:               el.Name,
			Description:        el.Comment,
			Value:              el.Value,
			DeprecationMessage: el.DeprecationMessage,
		}
	}

	return ComplexTypeSpec{
		ObjectTypeSpec: ObjectTypeSpec{Type: pkg.marshalType(t.ElementType, false).Type},
		Enum:           values,
	}
}

func (pkg *Package) marshalResource(r *Resource) (ResourceSpec, error) {
	object, err := pkg.marshalObjectData(r.Comment, r.Properties, r.Language, true)
	if err != nil {
		return ResourceSpec{}, fmt.Errorf("marshaling properties: %w", err)
	}

	requiredInputs, inputs, err := pkg.marshalProperties(r.InputProperties, false)
	if err != nil {
		return ResourceSpec{}, fmt.Errorf("marshaling input properties: %w", err)
	}

	var stateInputs *ObjectTypeSpec
	if r.StateInputs != nil {
		o, err := pkg.marshalObject(r.StateInputs, false)
		if err != nil {
			return ResourceSpec{}, fmt.Errorf("marshaling state inputs: %w", err)
		}
		stateInputs = &o.ObjectTypeSpec
	}

	var aliases []AliasSpec
	for _, a := range r.Aliases {
		aliases = append(aliases, AliasSpec{
			Name:    a.Name,
			Project: a.Project,
			Type:    a.Type,
		})
	}

	var methods map[string]string
	if len(r.Methods) != 0 {
		methods = map[string]string{}
		for _, m := range r.Methods {
			methods[m.Name] = m.Function.Token
		}
	}

	return ResourceSpec{
		ObjectTypeSpec:     object,
		InputProperties:    inputs,
		RequiredInputs:     requiredInputs,
		StateInputs:        stateInputs,
		Aliases:            aliases,
		DeprecationMessage: r.DeprecationMessage,
		IsComponent:        r.IsComponent,
		Methods:            methods,
	}, nil
}

func (pkg *Package) marshalFunction(f *Function) (FunctionSpec, error) {
	var inputs *ObjectTypeSpec
	if f.Inputs != nil {
		ins, err := pkg.marshalObject(f.Inputs, true)
		if err != nil {
			return FunctionSpec{}, fmt.Errorf("marshaling inputs: %w", err)
		}
		inputs = &ins.ObjectTypeSpec
	}

	var outputs *ObjectTypeSpec
	if f.Outputs != nil {
		outs, err := pkg.marshalObject(f.Outputs, true)
		if err != nil {
			return FunctionSpec{}, fmt.Errorf("marshaloutg outputs: %w", err)
		}
		outputs = &outs.ObjectTypeSpec
	}

	lang, err := marshalLanguage(f.Language)
	if err != nil {
		return FunctionSpec{}, err
	}

	return FunctionSpec{
		Description: f.Comment,
		Inputs:      inputs,
		Outputs:     outputs,
		Language:    lang,
	}, nil
}

func (pkg *Package) marshalProperties(props []*Property, plain bool) (required []string, specs map[string]PropertySpec,
	err error) {

	if len(props) == 0 {
		return
	}

	specs = make(map[string]PropertySpec, len(props))
	for _, p := range props {
		typ := p.Type
		if t, optional := typ.(*OptionalType); optional {
			typ = t.ElementType
		} else {
			required = append(required, p.Name)
		}

		var defaultValue interface{}
		var defaultSpec *DefaultSpec
		if p.DefaultValue != nil {
			defaultValue = p.DefaultValue.Value
			if len(p.DefaultValue.Environment) != 0 || len(p.DefaultValue.Language) != 0 {
				lang, err := marshalLanguage(p.DefaultValue.Language)
				if err != nil {
					return nil, nil, fmt.Errorf("property '%v': %w", p.Name, err)
				}

				defaultSpec = &DefaultSpec{
					Environment: p.DefaultValue.Environment,
					Language:    lang,
				}
			}
		}

		lang, err := marshalLanguage(p.Language)
		if err != nil {
			return nil, nil, fmt.Errorf("property '%v': %w", p.Name, err)
		}

		specs[p.Name] = PropertySpec{
			TypeSpec:           pkg.marshalType(typ, plain),
			Description:        p.Comment,
			Const:              p.ConstValue,
			Default:            defaultValue,
			DefaultInfo:        defaultSpec,
			DeprecationMessage: p.DeprecationMessage,
			Language:           lang,
			Secret:             p.Secret,
		}
	}
	return required, specs, nil
}

// marshalType marshals the given type into a TypeSpec. If plain is true, then the type is being marshaled within a
// plain type context (e.g. a resource output property or a function input/output object type), and therefore does not
// require `Plain` annotations (hence the odd-looking `Plain: !plain` fields below).
func (pkg *Package) marshalType(t Type, plain bool) TypeSpec {
	switch t := t.(type) {
	case *InputType:
		el := pkg.marshalType(t.ElementType, plain)
		el.Plain = false
		return el
	case *ArrayType:
		el := pkg.marshalType(t.ElementType, plain)
		return TypeSpec{
			Type:  "array",
			Items: &el,
			Plain: !plain,
		}
	case *MapType:
		el := pkg.marshalType(t.ElementType, plain)
		return TypeSpec{
			Type:                 "object",
			AdditionalProperties: &el,
			Plain:                !plain,
		}
	case *UnionType:
		oneOf := make([]TypeSpec, len(t.ElementTypes))
		for i, el := range t.ElementTypes {
			oneOf[i] = pkg.marshalType(el, plain)
		}

		defaultType := ""
		if t.DefaultType != nil {
			defaultType = pkg.marshalType(t.DefaultType, plain).Type
		}

		var discriminator *DiscriminatorSpec
		if t.Discriminator != "" {
			discriminator = &DiscriminatorSpec{
				PropertyName: t.Discriminator,
				Mapping:      t.Mapping,
			}
		}

		return TypeSpec{
			Type:          defaultType,
			OneOf:         oneOf,
			Discriminator: discriminator,
			Plain:         !plain,
		}
	case *ObjectType:
		return TypeSpec{Ref: pkg.marshalTypeRef(t.Package, "types", t.Token)}
	case *EnumType:
		return TypeSpec{Ref: pkg.marshalTypeRef(t.Package, "types", t.Token)}
	case *ResourceType:
		return TypeSpec{Ref: pkg.marshalTypeRef(t.Resource.Package, "resources", t.Token)}
	case *TokenType:
		var defaultType string
		if t.UnderlyingType != nil {
			defaultType = pkg.marshalType(t.UnderlyingType, plain).Type
		}

		return TypeSpec{
			Type: defaultType,
			Ref:  t.Token,
		}
	default:
		switch t {
		case BoolType:
			return TypeSpec{Type: "boolean"}
		case StringType:
			return TypeSpec{Type: "string"}
		case IntType:
			return TypeSpec{Type: "integer"}
		case NumberType:
			return TypeSpec{Type: "number"}
		case AnyType:
			return TypeSpec{Ref: "pulumi.json#/Any"}
		case ArchiveType:
			return TypeSpec{Ref: "pulumi.json#/Archive"}
		case AssetType:
			return TypeSpec{Ref: "pulumi.json#/Asset"}
		case JSONType:
			return TypeSpec{Ref: "pulumi.json#/Json"}
		default:
			panic(fmt.Errorf("unexepcted type %v (%T)", t, t))
		}
	}
}

func (pkg *Package) marshalTypeRef(container *Package, section, token string) string {
	token = url.PathEscape(token)

	if container == pkg {
		return fmt.Sprintf("#/%s/%s", section, token)
	}

	// TODO(schema): this isn't quite right--it doesn't handle schemas sourced from URLs--but it's good enough for now.
	return fmt.Sprintf("/%s/%v/schema.json#/%s/%s", container.Name, container.Version, section, token)
}

func marshalLanguage(lang map[string]interface{}) (map[string]RawMessage, error) {
	if len(lang) == 0 {
		return nil, nil
	}

	result := map[string]RawMessage{}
	for name, data := range lang {
		bytes, err := jsonMarshal(data)
		if err != nil {
			return nil, fmt.Errorf("marshaling %v language data: %w", name, err)
		}
		result[name] = RawMessage(bytes)
	}
	return result, nil
}

type RawMessage []byte

func (m RawMessage) MarshalJSON() ([]byte, error) {
	return []byte(m), nil
}

func (m *RawMessage) UnmarshalJSON(bytes []byte) error {
	*m = make([]byte, len(bytes))
	copy(*m, bytes)
	return nil
}

func (m RawMessage) MarshalYAML() ([]byte, error) {
	return []byte(m), nil
}

func (m *RawMessage) UnmarshalYAML(node *yaml.Node) error {
	var value interface{}
	if err := node.Decode(&value); err != nil {
		return err
	}
	bytes, err := jsonMarshal(value)
	if err != nil {
		return err
	}
	*m = bytes
	return nil
}

//go:embed pulumi.json
var metaSchema []byte

var MetaSchema *jsonschema.Schema

func init() {
	compiler := jsonschema.NewCompiler()
	compiler.LoadURL = func(u string) (io.ReadCloser, error) {
		if u == "blob://pulumi.json" {
			return io.NopCloser(bytes.NewReader(metaSchema)), nil
		}
		return jsonschema.LoadURL(u)
	}
	MetaSchema = compiler.MustCompile("blob://pulumi.json")
}

// TypeSpec is the serializable form of a reference to a type.
type TypeSpec struct {
	// Type is the primitive or composite type, if any. May be "bool", "integer", "number", "string", "array", or
	// "object".
	Type string `json:"type,omitempty" yaml:"type,omitempty"`
	// Ref is a reference to a type in this or another document. For example, the built-in Archive, Asset, and Any
	// types are referenced as "pulumi.json#/Archive", "pulumi.json#/Asset", and "pulumi.json#/Any", respectively.
	// A type from this document is referenced as "#/types/pulumi:type:token".
	// A type from another document is referenced as "path#/types/pulumi:type:token", where path is of the form:
	//   "/provider/vX.Y.Z/schema.json" or "pulumi.json" or "http[s]://example.com/provider/vX.Y.Z/schema.json"
	// A resource from this document is referenced as "#/resources/pulumi:type:token".
	// A resource from another document is referenced as "path#/resources/pulumi:type:token", where path is of the form:
	//   "/provider/vX.Y.Z/schema.json" or "pulumi.json" or "http[s]://example.com/provider/vX.Y.Z/schema.json"
	Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"`
	// AdditionalProperties, if set, describes the element type of an "object" (i.e. a string -> value map).
	AdditionalProperties *TypeSpec `json:"additionalProperties,omitempty" yaml:"additionalProperties,omitempty"`
	// Items, if set, describes the element type of an array.
	Items *TypeSpec `json:"items,omitempty" yaml:"items,omitempty"`
	// OneOf indicates that values of the type may be one of any of the listed types.
	OneOf []TypeSpec `json:"oneOf,omitempty" yaml:"oneOf,omitempty"`
	// Discriminator informs the consumer of an alternative schema based on the value associated with it.
	Discriminator *DiscriminatorSpec `json:"discriminator,omitempty" yaml:"discriminator,omitempty"`
	// Plain indicates that when used as an input, this type does not accept eventual values.
	Plain bool `json:"plain,omitempty" yaml:"plain,omitempty"`
}

// DiscriminatorSpec informs the consumer of an alternative schema based on the value associated with it.
type DiscriminatorSpec struct {
	// PropertyName is the name of the property in the payload that will hold the discriminator value.
	PropertyName string `json:"propertyName" yaml:"propertyName"`
	// Mapping is an optional object to hold mappings between payload values and schema names or references.
	Mapping map[string]string `json:"mapping,omitempty" yaml:"mapping,omitempty"`
}

// DefaultSpec is the serializable form of extra information about the default value for a property.
type DefaultSpec struct {
	// Environment specifies a set of environment variables to probe for a default value.
	Environment []string `json:"environment,omitempty" yaml:"environment,omitempty"`
	// Language specifies additional language-specific data about the default value.
	Language map[string]RawMessage `json:"language,omitempty" yaml:"language,omitempty"`
}

// PropertySpec is the serializable form of an object or resource property.
type PropertySpec struct {
	TypeSpec `yaml:",inline"`

	// Description is the description of the property, if any.
	Description string `json:"description,omitempty" yaml:"description,omitempty"`
	// Const is the constant value for the property, if any. The type of the value must be assignable to the type of
	// the property.
	Const interface{} `json:"const,omitempty" yaml:"const,omitempty"`
	// Default is the default value for the property, if any. The type of the value must be assignable to the type of
	// the property.
	Default interface{} `json:"default,omitempty" yaml:"default,omitempty"`
	// DefaultInfo contains additional information about the property's default value, if any.
	DefaultInfo *DefaultSpec `json:"defaultInfo,omitempty" yaml:"defaultInfo,omitempty"`
	// DeprecationMessage indicates whether or not the property is deprecated.
	DeprecationMessage string `json:"deprecationMessage,omitempty" yaml:"deprecationMessage,omitempty"`
	// Language specifies additional language-specific data about the property.
	Language map[string]RawMessage `json:"language,omitempty" yaml:"language,omitempty"`
	// Secret specifies if the property is secret (default false).
	Secret bool `json:"secret,omitempty" yaml:"secret,omitempty"`
	// ReplaceOnChanges specifies if the property is to be replaced instead of updated (default false).
	ReplaceOnChanges bool `json:"replaceOnChanges,omitempty" yaml:"replaceOnChanges,omitempty"`
}

// ObjectTypeSpec is the serializable form of an object type.
type ObjectTypeSpec struct {
	// Description is the description of the type, if any.
	Description string `json:"description,omitempty" yaml:"description,omitempty"`
	// Properties, if present, is a map from property name to PropertySpec that describes the type's properties.
	Properties map[string]PropertySpec `json:"properties,omitempty" yaml:"properties,omitempty"`
	// Type must be "object" if this is an object type, or the underlying type for an enum.
	Type string `json:"type,omitempty" yaml:"type,omitempty"`
	// Required, if present, is a list of the names of an object type's required properties. These properties must be set
	// for inputs and will always be set for outputs.
	Required []string `json:"required,omitempty" yaml:"required,omitempty"`
	// Plain, was a list of the names of an object type's plain properties. This property is ignored: instead, property
	// types should be marked as plain where necessary.
	Plain []string `json:"plain,omitempty" yaml:"plain,omitempty"`
	// Language specifies additional language-specific data about the type.
	Language map[string]RawMessage `json:"language,omitempty" yaml:"language,omitempty"`
}

// ComplexTypeSpec is the serializable form of an object or enum type.
type ComplexTypeSpec struct {
	ObjectTypeSpec `yaml:",inline"`

	// Enum, if present, is the list of possible values for an enum type.
	Enum []EnumValueSpec `json:"enum,omitempty" yaml:"enum,omitempty"`
}

// EnumValuesSpec is the serializable form of the values metadata associated with an enum type.
type EnumValueSpec struct {
	// Name, if present, overrides the name of the enum value that would usually be derived from the value.
	Name string `json:"name,omitempty" yaml:"name,omitempty"`
	// Description of the enum value.
	Description string `json:"description,omitempty" yaml:"description,omitempty"`
	// Value is the enum value itself.
	Value interface{} `json:"value" yaml:"value"`
	// DeprecationMessage indicates whether or not the value is deprecated.
	DeprecationMessage string `json:"deprecationMessage,omitempty" yaml:"deprecationMessage,omitempty"`
}

// AliasSpec is the serializable form of an alias description.
type AliasSpec struct {
	// Name is the name portion of the alias, if any.
	Name *string `json:"name,omitempty" yaml:"name,omitempty"`
	// Project is the project portion of the alias, if any.
	Project *string `json:"project,omitempty" yaml:"project,omitempty"`
	// Type is the type portion of the alias, if any.
	Type *string `json:"type,omitempty" yaml:"type,omitempty"`
}

// ResourceSpec is the serializable form of a resource description.
type ResourceSpec struct {
	ObjectTypeSpec `yaml:",inline"`

	// InputProperties is a map from property name to PropertySpec that describes the resource's input properties.
	InputProperties map[string]PropertySpec `json:"inputProperties,omitempty" yaml:"inputProperties,omitempty"`
	// RequiredInputs is a list of the names of the resource's required input properties.
	RequiredInputs []string `json:"requiredInputs,omitempty" yaml:"requiredInputs,omitempty"`
	// PlainInputs was a list of the names of the resource's plain input properties. This property is ignored:
	// instead, property types should be marked as plain where necessary.
	PlainInputs []string `json:"plainInputs,omitempty" yaml:"plainInputs,omitempty"`
	// StateInputs is an optional ObjectTypeSpec that describes additional inputs that mau be necessary to get an
	// existing resource. If this is unset, only an ID is necessary.
	StateInputs *ObjectTypeSpec `json:"stateInputs,omitempty" yaml:"stateInputs,omitempty"`
	// Aliases is the list of aliases for the resource.
	Aliases []AliasSpec `json:"aliases,omitempty" yaml:"aliases,omitempty"`
	// DeprecationMessage indicates whether or not the resource is deprecated.
	DeprecationMessage string `json:"deprecationMessage,omitempty" yaml:"deprecationMessage,omitempty"`
	// IsComponent indicates whether the resource is a ComponentResource.
	IsComponent bool `json:"isComponent,omitempty" yaml:"isComponent,omitempty"`
	// Methods maps method names to functions in this schema.
	Methods map[string]string `json:"methods,omitempty" yaml:"methods,omitempty"`
}

// FunctionSpec is the serializable form of a function description.
type FunctionSpec struct {
	// Description is the description of the function, if any.
	Description string `json:"description,omitempty" yaml:"description,omitempty"`
	// Inputs is the bag of input values for the function, if any.
	Inputs *ObjectTypeSpec `json:"inputs,omitempty" yaml:"inputs,omitempty"`
	// Outputs is the bag of output values for the function, if any.
	Outputs *ObjectTypeSpec `json:"outputs,omitempty" yaml:"outputs,omitempty"`
	// DeprecationMessage indicates whether or not the function is deprecated.
	DeprecationMessage string `json:"deprecationMessage,omitempty" yaml:"deprecationMessage,omitempty"`
	// Language specifies additional language-specific data about the function.
	Language map[string]RawMessage `json:"language,omitempty" yaml:"language,omitempty"`
}

// ConfigSpec is the serializable description of a package's configuration variables.
type ConfigSpec struct {
	// Variables is a map from variable name to PropertySpec that describes a package's configuration variables.
	Variables map[string]PropertySpec `json:"variables,omitempty" yaml:"variables,omitempty"`
	// Required is a list of the names of the package's required configuration variables.
	Required []string `json:"defaults,omitempty" yaml:"defaults,omitempty"`
}

// MetadataSpec contains information for the importer about this package.
type MetadataSpec struct {
	// ModuleFormat is a regex that is used by the importer to extract a module name from the module portion of a
	// type token. Packages that use the module format "namespace1/namespace2/.../namespaceN" do not need to specify
	// a format. The regex must define one capturing group that contains the module name, which must be formatted as
	// "namespace1/namespace2/...namespaceN".
	ModuleFormat string `json:"moduleFormat,omitempty" yaml:"moduleFormat,omitempty"`
}

// PackageSpec is the serializable description of a Pulumi package.
type PackageSpec struct {
	// Name is the unqualified name of the package (e.g. "aws", "azure", "gcp", "kubernetes", "random")
	Name string `json:"name" yaml:"name"`
	// Version is the version of the package. The version must be valid semver.
	Version string `json:"version,omitempty" yaml:"version,omitempty"`
	// Description is the description of the package.
	Description string `json:"description,omitempty" yaml:"description,omitempty"`
	// Keywords is the list of keywords that are associated with the package, if any.
	Keywords []string `json:"keywords,omitempty" yaml:"keywords,omitempty"`
	// Homepage is the package's homepage.
	Homepage string `json:"homepage,omitempty" yaml:"homepage,omitempty"`
	// License indicates which license is used for the package's contents.
	License string `json:"license,omitempty" yaml:"license,omitempty"`
	// Attribution allows freeform text attribution of derived work, if needed.
	Attribution string `json:"attribution,omitempty" yaml:"attribution,omitempty"`
	// Repository is the URL at which the source for the package can be found.
	Repository string `json:"repository,omitempty" yaml:"repository,omitempty"`
	// LogoURL is the URL for the package's logo, if any.
	LogoURL string `json:"logoUrl,omitempty" yaml:"logoUrl,omitempty"`
	// PluginDownloadURL is the URL to use to acquire the provider plugin binary, if any.
	PluginDownloadURL string `json:"pluginDownloadURL,omitempty" yaml:"pluginDownloadURL,omitempty"`

	// Meta contains information for the importer about this package.
	Meta *MetadataSpec `json:"meta,omitempty" yaml:"meta,omitempty"`

	// Config describes the set of configuration variables defined by this package.
	Config ConfigSpec `json:"config" yaml:"config"`
	// Types is a map from type token to ComplexTypeSpec that describes the set of complex types (ie. object, enum)
	// defined by this package.
	Types map[string]ComplexTypeSpec `json:"types,omitempty" yaml:"types,omitempty"`
	// Provider describes the provider type for this package.
	Provider ResourceSpec `json:"provider" yaml:"provider"`
	// Resources is a map from type token to ResourceSpec that describes the set of resources defined by this package.
	Resources map[string]ResourceSpec `json:"resources,omitempty" yaml:"resources,omitempty"`
	// Functions is a map from token to FunctionSpec that describes the set of functions defined by this package.
	Functions map[string]FunctionSpec `json:"functions,omitempty" yaml:"functions,omitempty"`
	// Language specifies additional language-specific data about the package.
	Language map[string]RawMessage `json:"language,omitempty" yaml:"language,omitempty"`
}

var defaultModuleFormat = regexp.MustCompile("(.*)")

func memberPath(section, token string, rest ...string) string {
	path := fmt.Sprintf("#/%v/%v", section, url.PathEscape(token))
	if len(rest) != 0 {
		path += "/" + strings.Join(rest, "/")
	}
	return path
}

func errorf(path, message string, args ...interface{}) *hcl.Diagnostic {
	contract.Require(path != "", "path")

	summary := path + ": " + fmt.Sprintf(message, args...)
	return &hcl.Diagnostic{
		Severity: hcl.DiagError,
		Summary:  summary,
	}
}

func validateSpec(spec PackageSpec) (hcl.Diagnostics, error) {
	bytes, err := json.Marshal(spec)
	if err != nil {
		return nil, err
	}
	var raw interface{}
	if err = json.Unmarshal(bytes, &raw); err != nil {
		return nil, err
	}

	if err = MetaSchema.Validate(raw); err == nil {
		return nil, nil
	}
	validationError, ok := err.(*jsonschema.ValidationError)
	if !ok {
		return nil, err
	}

	var diags hcl.Diagnostics
	var appendError func(err *jsonschema.ValidationError)
	appendError = func(err *jsonschema.ValidationError) {
		if err.InstanceLocation != "" && err.Message != "" {
			diags = diags.Append(errorf("#"+err.InstanceLocation, "%v", err.Message))
		}
		for _, err := range err.Causes {
			appendError(err)
		}
	}
	appendError(validationError)

	return diags, nil
}

// bindSpec converts a serializable PackageSpec into a Package. This function includes a loader parameter which
// works as a singleton -- if it is nil, a new loader is instantiated, else the provided loader is used. This avoids
// breaking downstream consumers of ImportSpec while allowing us to extend schema support to external packages.
//
// A few notes on diagnostics and errors in spec binding:
//
// - Unless an error is *fatal*--i.e. binding is fundamentally unable to proceed (e.g. because a provider for a package
//   failed to load)--errors should be communicated as diagnostics. Fatal errors should be communicated as error values.
// - Semantic errors during type binding should not be fatal. Instead, they should return an `InvalidType`. The invalid
//   type is accepted in any position, and carries diagnostics that explain the semantic error during binding. This
//   allows binding to continue and produce as much information as possible for the end user.
// - Diagnostics may be rendered to users by downstream tools, and should be written with schema authors in mind.
// - Diagnostics _must_ contain enough contextual information for a user to be able to understand the source of the
//   diagnostic. Until we have line/column information, we use JSON pointers to the offending entities. These pointers
//   are passed around using `path` parameters. The `errorf` function is provided as a utility to easily create a
//   diagnostic error that is appropriately tagged with a JSON pointer.
//
func bindSpec(spec PackageSpec, languages map[string]Language, loader Loader) (*Package, hcl.Diagnostics, error) {
	var diags hcl.Diagnostics

	// Validate the package against the metaschema.
	validationDiags, err := validateSpec(spec)
	if err != nil {
		return nil, nil, fmt.Errorf("validating spec: %w", err)
	}
	diags = diags.Extend(validationDiags)

	// Validate that there is a name
	if spec.Name == "" {
		diags = diags.Append(errorf("#/name", "no name provided"))
	}

	// Parse the version, if any.
	var version *semver.Version
	if spec.Version != "" {
		v, err := semver.ParseTolerant(spec.Version)
		if err != nil {
			diags = diags.Append(errorf("#/version", "failed to parse semver: %v", err))
		} else {
			version = &v
		}
	}

	// Parse the module format, if any.
	moduleFormat := "(.*)"
	if spec.Meta != nil && spec.Meta.ModuleFormat != "" {
		moduleFormat = spec.Meta.ModuleFormat
	}
	moduleFormatRegexp, err := regexp.Compile(moduleFormat)
	if err != nil {
		diags = diags.Append(errorf("#/meta/moduleFormat", "failed to compile regex: %v", err))
	}

	pkg := &Package{}

	// We want to use the same loader instance for all referenced packages, so only instantiate the loader if the
	// reference is nil.
	if loader == nil {
		cwd, err := os.Getwd()
		if err != nil {
			return nil, nil, err
		}
		ctx, err := plugin.NewContext(nil, nil, nil, nil, cwd, nil, false, nil)
		if err != nil {
			return nil, nil, err
		}
		defer contract.IgnoreClose(ctx)

		loader = NewPluginLoader(ctx.Host)
	}

	types, typeDiags, err := bindTypes(pkg, spec.Types, loader)
	if err != nil {
		return nil, nil, err
	}
	diags = diags.Extend(typeDiags)

	config, configDiags, err := bindConfig(spec.Config, types)
	if err != nil {
		return nil, nil, err
	}
	diags = diags.Extend(configDiags)

	functions, functionTable, functionDiags, err := bindFunctions(spec.Functions, types)
	if err != nil {
		return nil, nil, err
	}
	diags = diags.Extend(functionDiags)

	provider, providerDiags, err := bindProvider(spec.Name, spec.Provider, types, functionTable)
	if err != nil {
		return nil, nil, err
	}
	diags = diags.Extend(providerDiags)

	resources, resourceTable, resourceDiags, err := bindResources(spec.Resources, types, functionTable)
	if err != nil {
		return nil, nil, err
	}
	diags = diags.Extend(resourceDiags)

	// Build the type list.
	var typeList []Type
	for _, t := range types.resources {
		typeList = append(typeList, t)
	}
	for _, t := range types.objects {
		// t is a plain shape: add it and its corresponding input shape to the type list.
		typeList = append(typeList, t)
		typeList = append(typeList, t.InputShape)
	}
	for _, t := range types.arrays {
		typeList = append(typeList, t)
	}
	for _, t := range types.maps {
		typeList = append(typeList, t)
	}
	for _, t := range types.unions {
		typeList = append(typeList, t)
	}
	for _, t := range types.tokens {
		typeList = append(typeList, t)
	}
	for _, t := range types.enums {
		typeList = append(typeList, t)
	}

	sort.Slice(typeList, func(i, j int) bool {
		return typeList[i].String() < typeList[j].String()
	})

	language := make(map[string]interface{})
	for name, raw := range spec.Language {
		language[name] = json.RawMessage(raw)
	}

	*pkg = Package{
		moduleFormat:      moduleFormatRegexp,
		Name:              spec.Name,
		Version:           version,
		Description:       spec.Description,
		Keywords:          spec.Keywords,
		Homepage:          spec.Homepage,
		License:           spec.License,
		Attribution:       spec.Attribution,
		Repository:        spec.Repository,
		PluginDownloadURL: spec.PluginDownloadURL,
		Config:            config,
		Types:             typeList,
		Provider:          provider,
		Resources:         resources,
		Functions:         functions,
		Language:          language,
		resourceTable:     resourceTable,
		functionTable:     functionTable,
		typeTable:         types.named,
		resourceTypeTable: types.resources,
	}
	if err := pkg.ImportLanguages(languages); err != nil {
		return nil, nil, err
	}
	return pkg, diags, nil

}

// BindSpec converts a serializable PackageSpec into a Package. Any semantic errors encountered during binding are
// contained in the returned diagnostics. The returned error is only non-nil if a fatal error was encountered.
func BindSpec(spec PackageSpec, languages map[string]Language) (*Package, hcl.Diagnostics, error) {
	return bindSpec(spec, languages, nil)
}

// ImportSpec converts a serializable PackageSpec into a Package.
func ImportSpec(spec PackageSpec, languages map[string]Language) (*Package, error) {
	// Call the internal implementation that includes a loader parameter.
	pkg, diags, err := bindSpec(spec, languages, nil)
	if err != nil {
		return nil, err
	}
	if diags.HasErrors() {
		return nil, diags
	}
	return pkg, nil
}

// types facilitates interning (only storing a single reference to an object) during schema processing. The fields
// correspond to fields in the schema, and are populated during the binding process.
type types struct {
	pkg    *Package
	loader Loader

	resources map[string]*ResourceType
	objects   map[string]*ObjectType
	arrays    map[Type]*ArrayType
	maps      map[Type]*MapType
	unions    map[string]*UnionType
	tokens    map[string]*TokenType
	enums     map[string]*EnumType
	named     map[string]Type // objects and enums
	inputs    map[Type]*InputType
	optionals map[Type]*OptionalType
}

func (t *types) bindPrimitiveType(path, name string) (Type, hcl.Diagnostics) {
	switch name {
	case "boolean":
		return BoolType, nil
	case "integer":
		return IntType, nil
	case "number":
		return NumberType, nil
	case "string":
		return StringType, nil
	default:
		return invalidType(errorf(path, "unknown primitive type %v", name))
	}
}

// typeSpecRef contains the parsed fields from a type spec reference.
type typeSpecRef struct {
	URL *url.URL // The parsed URL

	Package string          // The package component of the schema ref
	Version *semver.Version // The version component of the schema ref

	Kind  string // The kind of reference: 'resources', 'types', or 'provider'
	Token string // The type token
}

const (
	resourcesRef = "resources"
	typesRef     = "types"
	providerRef  = "provider"
)

// Regex used to parse external schema paths. This is declared at the package scope to avoid repeated recompilation.
var refPathRegex = regexp.MustCompile(`^/?(?P<package>[-\w]+)/(?P<version>v[^/]*)/schema\.json$`)

func (t *types) parseTypeSpecRef(refPath, ref string) (typeSpecRef, hcl.Diagnostics) {
	parsedURL, err := url.Parse(ref)
	if err != nil {
		return typeSpecRef{}, hcl.Diagnostics{errorf(refPath, "failed to parse ref URL '%s': %v", ref, err)}
	}

	// Parse the package name and version if the URL contains a path. If there is no path--if the URL is just a
	// fragment--then the reference refers to the package being bound.
	pkgName, pkgVersion := t.pkg.Name, t.pkg.Version
	if len(parsedURL.Path) > 0 {
		path, err := url.PathUnescape(parsedURL.Path)
		if err != nil {
			return typeSpecRef{}, hcl.Diagnostics{errorf(refPath, "failed to unescape path '%s': %v", parsedURL.Path, err)}
		}

		pathMatch := refPathRegex.FindStringSubmatch(path)
		if len(pathMatch) != 3 {
			return typeSpecRef{}, hcl.Diagnostics{errorf(refPath, "failed to parse path '%s'", path)}
		}

		pkg, versionToken := pathMatch[1], pathMatch[2]
		version, err := semver.ParseTolerant(versionToken)
		if err != nil {
			return typeSpecRef{}, hcl.Diagnostics{errorf(refPath, "failed to parse package version '%s': %v", versionToken, err)}
		}

		pkgName, pkgVersion = pkg, &version
	}

	// Parse the fragment into a reference kind and token. The fragment is in one of two forms:
	// 1. #/provider
	// 2. #/(resources|types)/some:type:token
	//
	// Unfortunately, early code generators were lax and emitted unescaped backslashes in the type token, so we can't
	// just split on "/".
	fragment := path.Clean(parsedURL.EscapedFragment())
	if path.IsAbs(fragment) {
		fragment = fragment[1:]
	}

	kind, token := "", ""
	slash := strings.Index(fragment, "/")
	if slash == -1 {
		kind = fragment
	} else {
		kind, token = fragment[:slash], fragment[slash+1:]
	}

	switch kind {
	case "provider":
		if token != "" {
			return typeSpecRef{}, hcl.Diagnostics{errorf(refPath, "invalid provider reference '%v'", ref)}
		}
		token = "pulumi:providers:" + pkgName
	case "resources", "types":
		token, err = url.PathUnescape(token)
		if err != nil {
			return typeSpecRef{}, hcl.Diagnostics{errorf(refPath, "failed to unescape token '%s': %v", token, err)}
		}
	default:
		return typeSpecRef{}, hcl.Diagnostics{errorf(refPath, "invalid type reference '%v'", ref)}
	}

	return typeSpecRef{
		URL:     parsedURL,
		Package: pkgName,
		Version: pkgVersion,
		Kind:    kind,
		Token:   token,
	}, nil
}

func versionEquals(a, b *semver.Version) bool {
	// We treat "nil" as "unconstrained".
	if a == nil || b == nil {
		return true
	}
	return a.Equals(*b)
}

func (t *types) newInputType(elementType Type) Type {
	if _, ok := elementType.(*InputType); ok {
		return elementType
	}

	typ, ok := t.inputs[elementType]
	if !ok {
		typ = &InputType{ElementType: elementType}
		t.inputs[elementType] = typ
	}
	return typ
}

func (t *types) newOptionalType(elementType Type) Type {
	if _, ok := elementType.(*OptionalType); ok {
		return elementType
	}
	typ, ok := t.optionals[elementType]
	if !ok {
		typ = &OptionalType{ElementType: elementType}
		t.optionals[elementType] = typ
	}
	return typ
}

func (t *types) newMapType(elementType Type) Type {
	typ, ok := t.maps[elementType]
	if !ok {
		typ = &MapType{ElementType: elementType}
		t.maps[elementType] = typ
	}
	return typ
}

func (t *types) newArrayType(elementType Type) Type {
	typ, ok := t.arrays[elementType]
	if !ok {
		typ = &ArrayType{ElementType: elementType}
		t.arrays[elementType] = typ
	}
	return typ
}

func (t *types) newUnionType(
	elements []Type, defaultType Type, discriminator string, mapping map[string]string) *UnionType {
	union := &UnionType{
		ElementTypes:  elements,
		DefaultType:   defaultType,
		Discriminator: discriminator,
		Mapping:       mapping,
	}
	if typ, ok := t.unions[union.String()]; ok {
		return typ
	}
	t.unions[union.String()] = union
	return union
}

func (t *types) bindTypeSpecRef(path string, spec TypeSpec, inputShape bool) (Type, hcl.Diagnostics, error) {
	path = path + "/$ref"

	// Explicitly handle built-in types so that we don't have to handle this type of path during ref parsing.
	switch spec.Ref {
	case "pulumi.json#/Archive":
		return ArchiveType, nil, nil
	case "pulumi.json#/Asset":
		return AssetType, nil, nil
	case "pulumi.json#/Json":
		return JSONType, nil, nil
	case "pulumi.json#/Any":
		return AnyType, nil, nil
	}

	ref, refDiags := t.parseTypeSpecRef(path, spec.Ref)
	if refDiags.HasErrors() {
		typ, _ := invalidType(refDiags...)
		return typ, refDiags, nil
	}

	// If this is a reference to an external sch
	referencesExternalSchema := ref.Package != t.pkg.Name || !versionEquals(ref.Version, t.pkg.Version)
	if referencesExternalSchema {
		pkg, err := t.loader.LoadPackage(ref.Package, ref.Version)
		if err != nil {
			return nil, nil, fmt.Errorf("resolving package %v: %w", ref.URL, err)
		}

		switch ref.Kind {
		case typesRef:
			typ, ok := pkg.GetType(ref.Token)
			if !ok {
				typ, diags := invalidType(errorf(path, "type %v not found in package %v", ref.Token, ref.Package))
				return typ, diags, nil
			}
			if obj, ok := typ.(*ObjectType); ok && inputShape {
				typ = obj.InputShape
			}
			return typ, nil, nil
		case resourcesRef, providerRef:
			typ, ok := pkg.GetResourceType(ref.Token)
			if !ok {
				typ, diags := invalidType(errorf(path, "resource type %v not found in package %v", ref.Token, ref.Package))
				return typ, diags, nil
			}
			return typ, nil, nil
		}
	}

	switch ref.Kind {
	case typesRef:
		if typ, ok := t.objects[ref.Token]; ok {
			if inputShape {
				return typ.InputShape, nil, nil
			}
			return typ, nil, nil
		}
		if typ, ok := t.enums[ref.Token]; ok {
			return typ, nil, nil
		}

		var diags hcl.Diagnostics
		typ, ok := t.tokens[ref.Token]
		if !ok {
			typ = &TokenType{Token: ref.Token}
			if spec.Type != "" {
				ut, primDiags := t.bindPrimitiveType(path, spec.Type)
				diags = diags.Extend(primDiags)

				typ.UnderlyingType = ut
			}
			t.tokens[ref.Token] = typ
		}
		return typ, diags, nil
	case resourcesRef, providerRef:
		typ, ok := t.resources[ref.Token]
		if !ok {
			typ = &ResourceType{Token: ref.Token}
			t.resources[ref.Token] = typ
		}
		return typ, nil, nil
	default:
		typ, diags := invalidType(errorf(path, "failed to parse ref %s", spec.Ref))
		return typ, diags, nil
	}
}

func (t *types) bindType(path string, spec TypeSpec, inputShape bool) (result Type, diags hcl.Diagnostics, err error) {
	if inputShape && !spec.Plain {
		defer func() {
			result = t.newInputType(result)
		}()
	}

	if spec.Ref != "" {
		return t.bindTypeSpecRef(path, spec, inputShape)
	}

	if spec.OneOf != nil {
		if len(spec.OneOf) < 2 {
			diags = diags.Append(errorf(path+"/oneOf", "oneOf should list at least two types"))
		}

		var defaultType Type
		if spec.Type != "" {
			dt, primDiags := t.bindPrimitiveType(path+"/type", spec.Type)
			diags = diags.Extend(primDiags)

			defaultType = dt
		}

		elements := make([]Type, len(spec.OneOf))
		for i, spec := range spec.OneOf {
			e, typDiags, err := t.bindType(fmt.Sprintf("%s/oneOf/%v", path, i), spec, inputShape)
			diags = diags.Extend(typDiags)

			if err != nil {
				return nil, diags, err
			}

			elements[i] = e
		}

		var discriminator string
		var mapping map[string]string
		if spec.Discriminator != nil {
			if spec.Discriminator.PropertyName == "" {
				diags = diags.Append(errorf(path, "discriminator must provide a property name"))
			}
			discriminator = spec.Discriminator.PropertyName
			mapping = spec.Discriminator.Mapping
		}

		return t.newUnionType(elements, defaultType, discriminator, mapping), diags, nil
	}

	// nolint: goconst
	switch spec.Type {
	case "boolean", "integer", "number", "string":
		typ, typDiags := t.bindPrimitiveType(path+"/type", spec.Type)
		diags = diags.Extend(typDiags)

		return typ, diags, nil
	case "array":
		if spec.Items == nil {
			diags = diags.Append(errorf(path, "missing \"items\" property in array type spec"))
			typ, _ := invalidType(diags...)
			return typ, diags, nil
		}

		elementType, elementDiags, err := t.bindType(path+"/items", *spec.Items, inputShape)
		diags = diags.Extend(elementDiags)
		if err != nil {
			return nil, diags, err
		}

		return t.newArrayType(elementType), diags, nil
	case "object":
		elementType, elementDiags, err := t.bindType(path, TypeSpec{Type: "string"}, inputShape)
		contract.Assert(len(elementDiags) == 0)
		contract.Assert(err == nil)

		if spec.AdditionalProperties != nil {
			et, elementDiags, err := t.bindType(path+"/additionalProperties", *spec.AdditionalProperties, inputShape)
			diags = diags.Extend(elementDiags)
			if err != nil {
				return nil, diags, err
			}

			elementType = et
		}

		return t.newMapType(elementType), diags, nil
	default:
		diags = diags.Append(errorf(path+"/type", "unknown type kind %v", spec.Type))
		typ, _ := invalidType(diags...)
		return typ, diags, nil
	}
}

func plainType(typ Type) Type {
	for {
		switch t := typ.(type) {
		case *InputType:
			typ = t.ElementType
		case *OptionalType:
			typ = t.ElementType
		case *ObjectType:
			if t.PlainShape == nil {
				return t
			}
			typ = t.PlainShape
		default:
			return t
		}
	}
}

func bindConstValue(path, kind string, value interface{}, typ Type) (interface{}, hcl.Diagnostics) {
	if value == nil {
		return nil, nil
	}

	typeError := func(expectedType string) hcl.Diagnostics {
		return hcl.Diagnostics{errorf(path, "invalid constant of type %T for %v %v", value, expectedType, kind)}
	}

	switch typ = plainType(typ); typ {
	case BoolType:
		if _, ok := value.(bool); !ok {
			return false, typeError("boolean")
		}
	case IntType:
		v, ok := value.(float64)
		if !ok {
			return 0, typeError("integer")
		}
		if math.Trunc(v) != v || v < math.MinInt32 || v > math.MaxInt32 {
			return 0, typeError("integer")
		}
		value = int32(v)
	case NumberType:
		if _, ok := value.(float64); !ok {
			return 0.0, typeError("number")
		}
	case StringType:
		if _, ok := value.(string); !ok {
			return 0.0, typeError("string")
		}
	default:
		if _, isInvalid := typ.(*InvalidType); isInvalid {
			return nil, nil
		}
		return nil, hcl.Diagnostics{errorf(path, "type %v cannot have a constant value; only booleans, integers, "+
			"numbers and strings may have constant values", typ)}
	}

	return value, nil
}

func bindDefaultValue(path string, value interface{}, spec *DefaultSpec, typ Type) (*DefaultValue, hcl.Diagnostics) {
	if value == nil && spec == nil {
		return nil, nil
	}

	var diags hcl.Diagnostics
	if value != nil {
		typ = plainType(typ)
		switch typ := typ.(type) {
		case *UnionType:
			if typ.DefaultType != nil {
				return bindDefaultValue(path, value, spec, typ.DefaultType)
			}
			for _, elementType := range typ.ElementTypes {
				v, diags := bindDefaultValue(path, value, spec, elementType)
				if !diags.HasErrors() {
					return v, diags
				}
			}
		case *EnumType:
			return bindDefaultValue(path, value, spec, typ.ElementType)
		}

		v, valueDiags := bindConstValue(path, "default", value, typ)
		diags = diags.Extend(valueDiags)
		value = v
	}

	dv := &DefaultValue{Value: value}
	if spec != nil {
		language := make(map[string]interface{})
		for name, raw := range spec.Language {
			language[name] = json.RawMessage(raw)
		}
		if len(spec.Environment) == 0 {
			diags = diags.Append(errorf(path, "Default must specify an environment"))
		}

		dv.Environment, dv.Language = spec.Environment, language
	}
	return dv, diags
}

// bindProperties binds the map of property specs and list of required properties into a sorted list of properties and
// a lookup table.
func (t *types) bindProperties(path string, properties map[string]PropertySpec, requiredPath string, required []string,
	inputShape bool) ([]*Property, map[string]*Property, hcl.Diagnostics, error) {

	var diags hcl.Diagnostics

	// Bind property types and constant or default values.
	propertyMap := map[string]*Property{}
	var result []*Property
	for name, spec := range properties {
		propertyPath := path + "/" + name

		typ, typDiags, err := t.bindType(propertyPath, spec.TypeSpec, inputShape)
		diags = diags.Extend(typDiags)
		if err != nil {
			return nil, nil, diags, fmt.Errorf("error binding type for property %q: %w", name, err)
		}

		cv, cvDiags := bindConstValue(propertyPath+"/const", "constant", spec.Const, typ)
		diags = diags.Extend(cvDiags)

		dv, dvDiags := bindDefaultValue(propertyPath+"/default", spec.Default, spec.DefaultInfo, typ)
		diags = diags.Extend(dvDiags)

		language := make(map[string]interface{})
		for name, raw := range spec.Language {
			language[name] = json.RawMessage(raw)
		}

		p := &Property{
			Name:               name,
			Comment:            spec.Description,
			Type:               t.newOptionalType(typ),
			ConstValue:         cv,
			DefaultValue:       dv,
			DeprecationMessage: spec.DeprecationMessage,
			Language:           language,
			Secret:             spec.Secret,
			ReplaceOnChanges:   spec.ReplaceOnChanges,
		}

		propertyMap[name], result = p, append(result, p)
	}

	// Compute required properties.
	for i, name := range required {
		p, ok := propertyMap[name]
		if !ok {
			diags = diags.Append(errorf(fmt.Sprintf("%s/%v", requiredPath, i), "unknown required property %q", name))
			continue
		}
		if typ, ok := p.Type.(*OptionalType); ok {
			p.Type = typ.ElementType
		}
	}

	sort.Slice(result, func(i, j int) bool {
		return result[i].Name < result[j].Name
	})

	return result, propertyMap, diags, nil
}

func (t *types) bindObjectTypeDetails(path string, obj *ObjectType, token string,
	spec ObjectTypeSpec) (hcl.Diagnostics, error) {

	var diags hcl.Diagnostics

	if len(spec.Plain) > 0 {
		diags = diags.Append(errorf(path+"/plain",
			"plain has been removed; the property type must be marked as plain instead"))
	}

	properties, propertyMap, propertiesDiags, err := t.bindProperties(path+"/properties", spec.Properties,
		path+"/required", spec.Required, false)
	diags = diags.Extend(propertiesDiags)
	if err != nil {
		return diags, err
	}

	inputProperties, inputPropertyMap, inputPropertiesDiags, err := t.bindProperties(
		path+"/properties", spec.Properties, path+"/required", spec.Required, true)
	diags = diags.Extend(inputPropertiesDiags)
	if err != nil {
		return diags, err
	}

	language := make(map[string]interface{})
	for name, raw := range spec.Language {
		language[name] = json.RawMessage(raw)
	}

	obj.Package = t.pkg
	obj.Token = token
	obj.Comment = spec.Description
	obj.Language = language
	obj.Properties = properties
	obj.properties = propertyMap

	obj.InputShape.Package = t.pkg
	obj.InputShape.Token = token
	obj.InputShape.Comment = spec.Description
	obj.InputShape.Language = language
	obj.InputShape.Properties = inputProperties
	obj.InputShape.properties = inputPropertyMap

	return diags, nil
}

func (t *types) bindObjectType(path, token string, spec ObjectTypeSpec) (*ObjectType, hcl.Diagnostics, error) {
	obj := &ObjectType{}
	obj.InputShape = &ObjectType{PlainShape: obj}

	diags, err := t.bindObjectTypeDetails(path, obj, token, spec)
	if err != nil {
		return nil, diags, err
	}
	return obj, diags, nil
}

func (t *types) bindResourceTypeDetails(obj *ResourceType, token string) error {
	obj.Token = token
	return nil
}

func (t *types) bindResourceType(token string) (*ResourceType, error) {
	r := &ResourceType{}
	if err := t.bindResourceTypeDetails(r, token); err != nil {
		return nil, err
	}
	return r, nil
}

func (t *types) bindEnumTypeDetails(enum *EnumType, token string, spec ComplexTypeSpec) hcl.Diagnostics {
	var diags hcl.Diagnostics

	typ, typDiags := t.bindPrimitiveType(memberPath("types", token, "type"), spec.Type)
	diags = diags.Extend(typDiags)

	switch typ {
	case StringType, IntType, NumberType, BoolType:
		// OK
	default:
		if _, isInvalid := typ.(*InvalidType); !isInvalid {
			diags = diags.Append(errorf(memberPath("types", token, "type"),
				"enums may only be of type string, integer, number or boolean"))
		}
	}

	values, valuesDiags := t.bindEnumValues(memberPath("types", token, "enum"), spec.Enum, typ)
	diags = diags.Extend(valuesDiags)

	enum.Package = t.pkg
	enum.Token = token
	enum.Elements = values
	enum.ElementType = typ
	enum.Comment = spec.Description

	return diags
}

func (t *types) bindEnumValues(path string, values []EnumValueSpec, typ Type) ([]*Enum, hcl.Diagnostics) {
	var enums []*Enum
	var diags hcl.Diagnostics

	for i, spec := range values {
		value, valueDiags := bindConstValue(fmt.Sprintf("%s/%v/value", path, i), "enum", spec.Value, typ)
		diags = diags.Extend(valueDiags)

		enum := &Enum{
			Value:              value,
			Comment:            spec.Description,
			Name:               spec.Name,
			DeprecationMessage: spec.DeprecationMessage,
		}
		enums = append(enums, enum)
	}
	return enums, diags
}

func (t *types) bindEnumType(token string, spec ComplexTypeSpec) (*EnumType, error) {
	enum := &EnumType{}
	if err := t.bindEnumTypeDetails(enum, token, spec); err != nil {
		return nil, err
	}
	return enum, nil
}

func bindTypes(pkg *Package, complexTypes map[string]ComplexTypeSpec, loader Loader) (*types, hcl.Diagnostics, error) {
	var diags hcl.Diagnostics

	typs := &types{
		pkg:       pkg,
		loader:    loader,
		resources: map[string]*ResourceType{},
		objects:   map[string]*ObjectType{},
		arrays:    map[Type]*ArrayType{},
		maps:      map[Type]*MapType{},
		unions:    map[string]*UnionType{},
		tokens:    map[string]*TokenType{},
		enums:     map[string]*EnumType{},
		named:     map[string]Type{},
		inputs:    map[Type]*InputType{},
		optionals: map[Type]*OptionalType{},
	}

	// Declare object and enum types before processing properties.
	for token, spec := range complexTypes {
		if spec.Type == "object" {
			// It's important that we set the token here. This package interns types so that they can be equality-compared
			// for identity. Types are interned based on their string representation, and the string representation of an
			// object type is its token. While this doesn't affect object types directly, it breaks the interning of types
			// that reference object types (e.g. arrays, maps, unions)
			typ := &ObjectType{Token: token}
			typ.InputShape = &ObjectType{Token: token, PlainShape: typ}
			typs.objects[token] = typ
			typs.named[token] = typ
		} else if len(spec.Enum) > 0 {
			typ := &EnumType{Token: token}
			typs.enums[token] = typ
			typs.named[token] = typ

			// Bind enums before object types because object type generation depends on enum values to be present.
			enumDiags := typs.bindEnumTypeDetails(typs.enums[token], token, spec)
			diags = diags.Extend(enumDiags)
		}
	}

	// Process resources.
	for _, r := range pkg.Resources {
		typs.resources[r.Token] = &ResourceType{Token: r.Token}
	}

	// Process object types.
	for token, spec := range complexTypes {
		if spec.Type == "object" {
			path := memberPath("types", token)
			objDiags, err := typs.bindObjectTypeDetails(path, typs.objects[token], token, spec.ObjectTypeSpec)
			diags = diags.Extend(objDiags)

			if err != nil {
				return nil, diags, fmt.Errorf("failed to bind type %s: %w", token, err)
			}
		}
	}

	return typs, diags, nil
}

func bindMethods(path, resourceToken string, methods map[string]string,
	functionTable map[string]*Function) ([]*Method, hcl.Diagnostics) {

	var diags hcl.Diagnostics

	names := make([]string, 0, len(methods))
	for name := range methods {
		names = append(names, name)
	}
	sort.Strings(names)

	result := make([]*Method, 0, len(methods))
	for _, name := range names {
		token := methods[name]

		methodPath := path + "/" + name

		function, ok := functionTable[token]
		if !ok {
			diags = diags.Append(errorf(methodPath, "unknown function %s", token))
			continue
		}
		if function.IsMethod {
			diags = diags.Append(errorf(methodPath, "function %s is already a method", token))
			continue
		}
		idx := strings.LastIndex(function.Token, "/")
		if idx == -1 || function.Token[:idx] != resourceToken {
			diags = diags.Append(errorf(methodPath, "invalid function token format %s", token))
			continue
		}
		if function.Inputs == nil || function.Inputs.Properties == nil || len(function.Inputs.Properties) == 0 ||
			function.Inputs.Properties[0].Name != "__self__" {
			diags = diags.Append(errorf(methodPath, "function %s has no __self__ parameter", token))
			continue
		}
		function.IsMethod = true
		result = append(result, &Method{
			Name:     name,
			Function: function,
		})
	}
	return result, diags
}

func bindConfig(spec ConfigSpec, types *types) ([]*Property, hcl.Diagnostics, error) {
	properties, _, diags, err := types.bindProperties("#/config/variables", spec.Variables,
		"#/config/defaults", spec.Required, false)
	return properties, diags, err
}

func bindResource(path, token string, spec ResourceSpec, types *types,
	functionTable map[string]*Function) (*Resource, hcl.Diagnostics, error) {

	var diags hcl.Diagnostics

	if len(spec.Plain) > 0 {
		diags = diags.Append(errorf(path+"/plain", "plain has been removed; property types must be marked as plain instead"))
	}
	if len(spec.PlainInputs) > 0 {
		diags = diags.Append(errorf(path+"/plainInputs",
			"plainInputs has been removed; individual property types must be marked as plain instead"))
	}

	properties, _, propertyDiags, err := types.bindProperties(path+"/properties", spec.Properties,
		path+"/required", spec.Required, false)
	diags = diags.Extend(propertyDiags)
	if err != nil {
		return nil, diags, fmt.Errorf("failed to bind properties for %v: %w", token, err)
	}

	inputProperties, _, inputDiags, err := types.bindProperties(path+"/inputProperties", spec.InputProperties,
		path+"/requiredInputs", spec.RequiredInputs, true)
	diags = diags.Extend(inputDiags)
	if err != nil {
		return nil, diags, fmt.Errorf("failed to bind input properties for %v: %w", token, err)
	}

	methods, methodDiags := bindMethods(path+"/methods", token, spec.Methods, functionTable)
	diags = diags.Extend(methodDiags)

	for _, method := range methods {
		if _, ok := spec.Properties[method.Name]; ok {
			diags = diags.Append(errorf(path+"/methods/"+method.Name, "%v already has a property named %s", token, method.Name))
		}
	}

	var stateInputs *ObjectType
	if spec.StateInputs != nil {
		si, stateDiags, err := types.bindObjectType(path+"/stateInputs", token+"Args", *spec.StateInputs)
		diags = diags.Extend(stateDiags)
		if err != nil {
			return nil, diags, fmt.Errorf("error binding inputs for %v: %w", token, err)
		}
		stateInputs = si.InputShape
	}

	var aliases []*Alias
	for _, a := range spec.Aliases {
		aliases = append(aliases, &Alias{Name: a.Name, Project: a.Project, Type: a.Type})
	}

	language := make(map[string]interface{})
	for name, raw := range spec.Language {
		language[name] = json.RawMessage(raw)
	}

	return &Resource{
		Package:            types.pkg,
		Token:              token,
		Comment:            spec.Description,
		InputProperties:    inputProperties,
		Properties:         properties,
		StateInputs:        stateInputs,
		Aliases:            aliases,
		DeprecationMessage: spec.DeprecationMessage,
		Language:           language,
		IsComponent:        spec.IsComponent,
		Methods:            methods,
	}, diags, nil
}

func bindProvider(pkgName string, spec ResourceSpec, types *types,
	functionTable map[string]*Function) (*Resource, hcl.Diagnostics, error) {

	res, diags, err := bindResource("#/provider", "pulumi:providers:"+pkgName, spec, types, functionTable)
	if err != nil {
		return nil, diags, fmt.Errorf("error binding provider: %w", err)
	}
	res.IsProvider = true

	// Since non-primitive provider configuration is currently JSON serialized, we can't handle it without
	// modifying the path by which it's looked up. As a temporary workaround to enable access to config which
	// values which are primitives, we'll simply remove any properties for the provider resource which are not
	// strings, or types with an underlying type of string, before we generate the provider code.
	var stringProperties []*Property
	for _, prop := range res.Properties {
		typ := plainType(prop.Type)
		if tokenType, isTokenType := typ.(*TokenType); isTokenType {
			if tokenType.UnderlyingType != stringType {
				continue
			}
		} else {
			if typ != stringType {
				continue
			}
		}

		stringProperties = append(stringProperties, prop)
	}
	res.Properties = stringProperties

	types.resources[res.Token] = &ResourceType{
		Token:    res.Token,
		Resource: res,
	}

	return res, diags, nil
}

func bindResources(specs map[string]ResourceSpec, types *types,
	functionTable map[string]*Function) ([]*Resource, map[string]*Resource, hcl.Diagnostics, error) {

	var diags hcl.Diagnostics

	resourceTable := map[string]*Resource{}
	var resources []*Resource
	for token, spec := range specs {
		res, resDiags, err := bindResource(memberPath("resources", token), token, spec, types, functionTable)
		diags = diags.Extend(resDiags)
		if err != nil {
			return nil, nil, diags, fmt.Errorf("error binding resource %v: %w", token, err)
		}
		resourceTable[token] = res

		if rt, ok := types.resources[token]; ok {
			if rt.Resource == nil {
				rt.Resource = res
			}
		} else {
			types.resources[token] = &ResourceType{
				Token:    res.Token,
				Resource: res,
			}
		}

		resources = append(resources, res)
	}

	sort.Slice(resources, func(i, j int) bool {
		return resources[i].Token < resources[j].Token
	})

	return resources, resourceTable, diags, nil
}

func bindFunction(token string, spec FunctionSpec, types *types) (*Function, hcl.Diagnostics, error) {
	var diags hcl.Diagnostics

	path := memberPath("functions", token)

	var inputs *ObjectType
	if spec.Inputs != nil {
		ins, inDiags, err := types.bindObjectType(path+"/inputs", token+"Args", *spec.Inputs)
		diags = diags.Extend(inDiags)
		if err != nil {
			return nil, diags, fmt.Errorf("error binding inputs for function %v: %w", token, err)
		}
		inputs = ins
	}

	var outputs *ObjectType
	if spec.Outputs != nil {
		outs, outDiags, err := types.bindObjectType(path+"/outputs", token+"Result", *spec.Outputs)
		diags = diags.Extend(outDiags)
		if err != nil {
			return nil, diags, fmt.Errorf("error binding outputs for function %v: %w", token, err)
		}
		outputs = outs
	}

	language := make(map[string]interface{})
	for name, raw := range spec.Language {
		language[name] = json.RawMessage(raw)
	}

	return &Function{
		Package:            types.pkg,
		Token:              token,
		Comment:            spec.Description,
		Inputs:             inputs,
		Outputs:            outputs,
		DeprecationMessage: spec.DeprecationMessage,
		Language:           language,
	}, diags, nil
}

func bindFunctions(specs map[string]FunctionSpec,
	types *types) ([]*Function, map[string]*Function, hcl.Diagnostics, error) {

	var diags hcl.Diagnostics

	functionTable := map[string]*Function{}
	var functions []*Function
	for token, spec := range specs {
		f, fdiags, err := bindFunction(token, spec, types)
		diags = diags.Extend(fdiags)
		if err != nil {
			return nil, nil, diags, fmt.Errorf("error binding function %v: %w", token, err)
		}
		functionTable[token] = f
		functions = append(functions, f)
	}

	sort.Slice(functions, func(i, j int) bool {
		return functions[i].Token < functions[j].Token
	})

	return functions, functionTable, diags, nil
}

func jsonMarshal(v interface{}) ([]byte, error) {
	var b bytes.Buffer
	enc := json.NewEncoder(&b)
	enc.SetEscapeHTML(false)
	enc.SetIndent("", "  ")
	if err := enc.Encode(v); err != nil {
		return nil, err
	}
	return b.Bytes(), nil
}

// Determines if codegen should emit a ${fn}Output version that
// automatically accepts Inputs and returns Outputs.
func (fun *Function) NeedsOutputVersion() bool {

	// Skip functions that return no value. Arguably we could
	// support them and return `Task`, but there are no such
	// functions in `pulumi-azure-native` or `pulumi-aws` so we
	// omit to simplify.
	if fun.Outputs == nil {
		return false
	}

	// Skip functions that have no inputs. The user can simply
	// lift the `Task` to `Output` manually.
	if fun.Inputs == nil {
		return false
	}

	// No properties is kind of like no inputs.
	if len(fun.Inputs.Properties) == 0 {
		return false
	}

	return true
}
