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

import (
	"github.com/hashicorp/hcl/v2"
	"github.com/pulumi/pulumi/pkg/v3/codegen/hcl2/model"
)

func getEntriesSignature(
	args []model.Expression,
	options bindOptions,
) (model.StaticFunctionSignature, hcl.Diagnostics) {
	var diagnostics hcl.Diagnostics

	keyType, valueType := model.Type(model.DynamicType), model.Type(model.DynamicType)
	signature := model.StaticFunctionSignature{
		Parameters: []model.Parameter{{
			Name: "collection",
			Type: model.DynamicType,
		}},
	}

	if len(args) == 1 {
		strictCollectionTypechecking := !options.skipRangeTypecheck
		keyT, valueT, diags := model.GetCollectionTypes(
			model.ResolveOutputs(args[0].Type()),
			args[0].SyntaxNode().Range(),
			strictCollectionTypechecking)
		keyType, valueType, diagnostics = keyT, valueT, append(diagnostics, diags...)
	}

	elementType := model.NewObjectType(map[string]model.Type{
		"key":   keyType,
		"value": valueType,
	})
	signature.ReturnType = model.NewListType(elementType)
	return signature, diagnostics
}

func pulumiBuiltins(options bindOptions) map[string]*model.Function {
	return map[string]*model.Function{
		"element": model.NewFunction(model.GenericFunctionSignature(
			func(args []model.Expression) (model.StaticFunctionSignature, hcl.Diagnostics) {
				var diagnostics hcl.Diagnostics

				listType, returnType := model.Type(model.DynamicType), model.Type(model.DynamicType)
				if len(args) > 0 {
					switch t := model.ResolveOutputs(args[0].Type()).(type) {
					case *model.ListType:
						listType, returnType = args[0].Type(), t.ElementType
					case *model.TupleType:
						_, elementType := model.UnifyTypes(t.ElementTypes...)
						listType, returnType = args[0].Type(), elementType
					default:
						rng := args[0].SyntaxNode().Range()
						diagnostics = hcl.Diagnostics{&hcl.Diagnostic{
							Severity: hcl.DiagError,
							Summary:  "the first argument to 'element' must be a list or tuple",
							Subject:  &rng,
						}}
					}
				}
				return model.StaticFunctionSignature{
					Parameters: []model.Parameter{
						{
							Name: "list",
							Type: listType,
						},
						{
							Name: "index",
							Type: model.NumberType,
						},
					},
					ReturnType: returnType,
				}, diagnostics
			})),
		"entries": model.NewFunction(model.GenericFunctionSignature(
			func(arguments []model.Expression) (model.StaticFunctionSignature, hcl.Diagnostics) {
				return getEntriesSignature(arguments, options)
			})),

		"fileArchive": model.NewFunction(model.StaticFunctionSignature{
			Parameters: []model.Parameter{{
				Name: "path",
				Type: model.StringType,
			}},
			ReturnType: ArchiveType,
		}),
		"remoteArchive": model.NewFunction(model.StaticFunctionSignature{
			Parameters: []model.Parameter{{
				Name: "uri",
				Type: model.StringType,
			}},
			ReturnType: ArchiveType,
		}),
		"assetArchive": model.NewFunction(model.StaticFunctionSignature{
			Parameters: []model.Parameter{{
				Name: "assets",
				Type: model.NewMapType(AssetOrArchiveType),
			}},
			ReturnType: ArchiveType,
		}),
		"fileAsset": model.NewFunction(model.StaticFunctionSignature{
			Parameters: []model.Parameter{{
				Name: "path",
				Type: model.StringType,
			}},
			ReturnType: AssetType,
		}),
		"stringAsset": model.NewFunction(model.StaticFunctionSignature{
			Parameters: []model.Parameter{{
				Name: "value",
				Type: model.StringType,
			}},
			ReturnType: AssetType,
		}),
		"remoteAsset": model.NewFunction(model.StaticFunctionSignature{
			Parameters: []model.Parameter{{
				Name: "uri",
				Type: model.StringType,
			}},
			ReturnType: AssetType,
		}),
		"join": model.NewFunction(model.StaticFunctionSignature{
			Parameters: []model.Parameter{
				{
					Name: "separator",
					Type: model.StringType,
				},
				{
					Name: "strings",
					Type: model.NewListType(model.StringType),
				},
			},
			ReturnType: model.StringType,
		}),
		"length": model.NewFunction(model.GenericFunctionSignature(
			func(args []model.Expression) (model.StaticFunctionSignature, hcl.Diagnostics) {
				var diagnostics hcl.Diagnostics

				valueType := model.Type(model.DynamicType)
				if len(args) > 0 {
					valueType = args[0].Type()
					elementType := UnwrapOption(model.ResolveOutputs(valueType))
					switch valueType := elementType.(type) {
					case *model.ListType, *model.MapType, *model.ObjectType, *model.TupleType:
						// OK
					default:
						if model.StringType.ConversionFrom(valueType) == model.NoConversion {
							rng := args[0].SyntaxNode().Range()
							diagnostics = hcl.Diagnostics{&hcl.Diagnostic{
								Severity: hcl.DiagError,
								Summary:  "the first argument to 'length' must be a list, map, object, tuple, or string",
								Subject:  &rng,
							}}
						}
					}
				}
				return model.StaticFunctionSignature{
					Parameters: []model.Parameter{{
						Name: "value",
						Type: valueType,
					}},
					ReturnType: model.IntType,
				}, diagnostics
			})),
		"lookup": model.NewFunction(model.GenericFunctionSignature(
			func(args []model.Expression) (model.StaticFunctionSignature, hcl.Diagnostics) {
				var diagnostics hcl.Diagnostics

				mapType, elementType := model.Type(model.DynamicType), model.Type(model.DynamicType)
				if len(args) > 0 {
					switch t := model.ResolveOutputs(args[0].Type()).(type) {
					case *model.MapType:
						mapType, elementType = args[0].Type(), t.ElementType
					case *model.ObjectType:
						var unifiedType model.Type
						for _, t := range t.Properties {
							_, unifiedType = model.UnifyTypes(unifiedType, t)
						}
						mapType, elementType = args[0].Type(), unifiedType
					default:
						rng := args[0].SyntaxNode().Range()
						diagnostics = hcl.Diagnostics{&hcl.Diagnostic{
							Severity: hcl.DiagError,
							Summary:  "the first argument to 'lookup' must be a map",
							Subject:  &rng,
						}}
					}
				}
				return model.StaticFunctionSignature{
					Parameters: []model.Parameter{
						{
							Name: "map",
							Type: mapType,
						},
						{
							Name: "key",
							Type: model.StringType,
						},
						{
							Name: "default",
							Type: model.NewOptionalType(elementType),
						},
					},
					ReturnType: elementType,
				}, diagnostics
			})),
		"mimeType": model.NewFunction(model.StaticFunctionSignature{
			Parameters: []model.Parameter{{
				Name: "path",
				Type: model.StringType,
			}},
			ReturnType: model.StringType,
		}),
		"range": model.NewFunction(model.StaticFunctionSignature{
			Parameters: []model.Parameter{
				{
					Name: "fromOrTo",
					Type: model.NumberType,
				},
				{
					Name: "to",
					Type: model.NewOptionalType(model.NumberType),
				},
			},
			ReturnType: model.NewListType(model.IntType),
		}),
		"readDir": model.NewFunction(model.StaticFunctionSignature{
			Parameters: []model.Parameter{{
				Name: "path",
				Type: model.StringType,
			}},
			ReturnType: model.NewListType(model.StringType),
		}),
		"readFile": model.NewFunction(model.StaticFunctionSignature{
			Parameters: []model.Parameter{{
				Name: "path",
				Type: model.StringType,
			}},
			ReturnType: model.StringType,
		}),
		"filebase64": model.NewFunction(model.StaticFunctionSignature{
			Parameters: []model.Parameter{{
				Name: "path",
				Type: model.StringType,
			}},
			ReturnType: model.StringType,
		}),
		"filebase64sha256": model.NewFunction(model.StaticFunctionSignature{
			Parameters: []model.Parameter{{
				Name: "path",
				Type: model.StringType,
			}},
			ReturnType: model.StringType,
		}),
		"secret": model.NewFunction(model.GenericFunctionSignature(
			func(args []model.Expression) (model.StaticFunctionSignature, hcl.Diagnostics) {
				valueType := model.Type(model.DynamicType)
				if len(args) == 1 {
					valueType = args[0].Type()
				}

				return model.StaticFunctionSignature{
					Parameters: []model.Parameter{{
						Name: "value",
						Type: valueType,
					}},
					ReturnType: model.NewOutputType(valueType),
				}, nil
			})),
		"unsecret": model.NewFunction(model.GenericFunctionSignature(
			func(args []model.Expression) (model.StaticFunctionSignature, hcl.Diagnostics) {
				valueType := model.Type(model.DynamicType)
				if len(args) == 1 {
					valueType = args[0].Type()
				}

				return model.StaticFunctionSignature{
					Parameters: []model.Parameter{{
						Name: "value",
						Type: valueType,
					}},
					ReturnType: model.NewOutputType(valueType),
				}, nil
			})),
		"sha1": model.NewFunction(model.StaticFunctionSignature{
			Parameters: []model.Parameter{{
				Name: "input",
				Type: model.StringType,
			}},
			ReturnType: model.StringType,
		}),
		"split": model.NewFunction(model.StaticFunctionSignature{
			Parameters: []model.Parameter{
				{
					Name: "separator",
					Type: model.StringType,
				},
				{
					Name: "string",
					Type: model.StringType,
				},
			},
			ReturnType: model.NewListType(model.StringType),
		}),
		"toBase64": model.NewFunction(model.StaticFunctionSignature{
			Parameters: []model.Parameter{{
				Name: "value",
				Type: model.StringType,
			}},
			ReturnType: model.StringType,
		}),
		"fromBase64": model.NewFunction(model.StaticFunctionSignature{
			Parameters: []model.Parameter{{
				Name: "value",
				Type: model.StringType,
			}},
			ReturnType: model.StringType,
		}),
		"toJSON": model.NewFunction(model.StaticFunctionSignature{
			Parameters: []model.Parameter{{
				Name: "value",
				Type: model.DynamicType,
			}},
			ReturnType: model.StringType,
		}),
		// Returns the name of the current stack
		"stack": model.NewFunction(model.StaticFunctionSignature{
			ReturnType: model.StringType,
		}),
		// Returns the name of the current project
		"project": model.NewFunction(model.StaticFunctionSignature{
			ReturnType: model.StringType,
		}),
		// Returns the directory from which pulumi was run
		"cwd": model.NewFunction(model.StaticFunctionSignature{
			ReturnType: model.StringType,
		}),
		"notImplemented": model.NewFunction(model.StaticFunctionSignature{
			Parameters: []model.Parameter{{
				Name: "errorMessage",
				Type: model.StringType,
			}},
			ReturnType: model.DynamicType,
		}),
		// Returns either the single item in a list, none if the list is empty or errors.
		"singleOrNone": model.NewFunction(model.GenericFunctionSignature(
			func(args []model.Expression) (model.StaticFunctionSignature, hcl.Diagnostics) {
				var diagnostics hcl.Diagnostics

				valueType := model.Type(model.DynamicType)
				if len(args) == 1 {
					valueType = args[0].Type()
				} else {
					diagnostics = hcl.Diagnostics{&hcl.Diagnostic{
						Severity: hcl.DiagError,
						Summary:  "'singleOrNone' only expects one argument",
					}}
				}

				elementType := model.Type(model.DynamicType)

				switch valueType := model.ResolveOutputs(valueType).(type) {
				case *model.ListType:
					elementType = valueType.ElementType
				case *model.TupleType:
					if len(valueType.ElementTypes) > 0 {
						elementType = valueType.ElementTypes[0]
					}
				default:
					if valueType != model.DynamicType {
						rng := args[0].SyntaxNode().Range()
						diagnostics = hcl.Diagnostics{&hcl.Diagnostic{
							Severity: hcl.DiagError,
							Summary:  "the first argument to 'singleOrNone' must be a list or tuple",
							Subject:  &rng,
						}}
					}
				}

				return model.StaticFunctionSignature{
					Parameters: []model.Parameter{{
						Name: "value",
						Type: valueType,
					}},
					ReturnType: model.NewOptionalType(elementType),
				}, diagnostics
			})),
		"getOutput": model.NewFunction(model.StaticFunctionSignature{
			Parameters: []model.Parameter{{
				Name: "stackReference",
				// TODO: should be StackReference resource type but I'm not sure how to define that
				Type: model.DynamicType,
			}, {
				Name: "outputName",
				Type: model.StringType,
			}},
			ReturnType: model.NewOutputType(model.DynamicType),
		}),
	}
}