// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package typeexpr

import (
	"bytes"
	"fmt"
	"sort"

	"github.com/hashicorp/hcl/v2/hclsyntax"

	"github.com/hashicorp/hcl/v2"
	"github.com/zclconf/go-cty/cty"
)

// Type attempts to process the given expression as a type expression and, if
// successful, returns the resulting type. If unsuccessful, error diagnostics
// are returned.
func Type(expr hcl.Expression) (cty.Type, hcl.Diagnostics) {
	ty, _, diags := getType(expr, false, false)
	return ty, diags
}

// TypeConstraint attempts to parse the given expression as a type constraint
// and, if successful, returns the resulting type. If unsuccessful, error
// diagnostics are returned.
//
// A type constraint has the same structure as a type, but it additionally
// allows the keyword "any" to represent cty.DynamicPseudoType, which is often
// used as a wildcard in type checking and type conversion operations.
func TypeConstraint(expr hcl.Expression) (cty.Type, hcl.Diagnostics) {
	ty, _, diags := getType(expr, true, false)
	return ty, diags
}

// TypeConstraintWithDefaults attempts to parse the given expression as a type
// constraint which may include default values for object attributes. If
// successful both the resulting type and corresponding defaults are returned.
// If unsuccessful, error diagnostics are returned.
func TypeConstraintWithDefaults(expr hcl.Expression) (cty.Type, *Defaults, hcl.Diagnostics) {
	return getType(expr, true, true)
}

// TypeString returns a string rendering of the given type as it would be
// expected to appear in the HCL native syntax.
//
// This is primarily intended for showing types to the user in an application
// that uses typexpr, where the user can be assumed to be familiar with the
// type expression syntax. In applications that do not use typeexpr these
// results may be confusing to the user and so type.FriendlyName may be
// preferable, even though it's less precise.
//
// TypeString produces reasonable results only for types like what would be
// produced by the Type and TypeConstraint functions. In particular, it cannot
// support capsule types.
func TypeString(ty cty.Type) string {
	// Easy cases first
	switch ty {
	case cty.String:
		return "string"
	case cty.Bool:
		return "bool"
	case cty.Number:
		return "number"
	case cty.DynamicPseudoType:
		return "any"
	}

	if ty.IsCapsuleType() {
		panic("TypeString does not support capsule types")
	}

	if ty.IsCollectionType() {
		ety := ty.ElementType()
		etyString := TypeString(ety)
		switch {
		case ty.IsListType():
			return fmt.Sprintf("list(%s)", etyString)
		case ty.IsSetType():
			return fmt.Sprintf("set(%s)", etyString)
		case ty.IsMapType():
			return fmt.Sprintf("map(%s)", etyString)
		default:
			// Should never happen because the above is exhaustive
			panic("unsupported collection type")
		}
	}

	if ty.IsObjectType() {
		var buf bytes.Buffer
		buf.WriteString("object({")
		atys := ty.AttributeTypes()
		names := make([]string, 0, len(atys))
		for name := range atys {
			names = append(names, name)
		}
		sort.Strings(names)
		first := true
		for _, name := range names {
			aty := atys[name]
			if !first {
				buf.WriteByte(',')
			}
			if !hclsyntax.ValidIdentifier(name) {
				// Should never happen for any type produced by this package,
				// but we'll do something reasonable here just so we don't
				// produce garbage if someone gives us a hand-assembled object
				// type that has weird attribute names.
				// Using Go-style quoting here isn't perfect, since it doesn't
				// exactly match HCL syntax, but it's fine for an edge-case.
				buf.WriteString(fmt.Sprintf("%q", name))
			} else {
				buf.WriteString(name)
			}
			buf.WriteByte('=')
			buf.WriteString(TypeString(aty))
			first = false
		}
		buf.WriteString("})")
		return buf.String()
	}

	if ty.IsTupleType() {
		var buf bytes.Buffer
		buf.WriteString("tuple([")
		etys := ty.TupleElementTypes()
		first := true
		for _, ety := range etys {
			if !first {
				buf.WriteByte(',')
			}
			buf.WriteString(TypeString(ety))
			first = false
		}
		buf.WriteString("])")
		return buf.String()
	}

	// Should never happen because we covered all cases above.
	panic(fmt.Errorf("unsupported type %#v", ty))
}
