package hcl

import (
	"fmt"
	"math/big"

	"github.com/zclconf/go-cty/cty"
	"github.com/zclconf/go-cty/cty/convert"
)

// Index is a helper function that performs the same operation as the index
// operator in the HCL expression language. That is, the result is the
// same as it would be for collection[key] in a configuration expression.
//
// This is exported so that applications can perform indexing in a manner
// consistent with how the language does it, including handling of null and
// unknown values, etc.
//
// Diagnostics are produced if the given combination of values is not valid.
// Therefore a pointer to a source range must be provided to use in diagnostics,
// though nil can be provided if the calling application is going to
// ignore the subject of the returned diagnostics anyway.
func Index(collection, key cty.Value, srcRange *Range) (cty.Value, Diagnostics) {
	if collection.IsNull() {
		return cty.DynamicVal, Diagnostics{
			{
				Severity: DiagError,
				Summary:  "Attempt to index null value",
				Detail:   "This value is null, so it does not have any indices.",
				Subject:  srcRange,
			},
		}
	}
	if key.IsNull() {
		return cty.DynamicVal, Diagnostics{
			{
				Severity: DiagError,
				Summary:  "Invalid index",
				Detail:   "Can't use a null value as an indexing key.",
				Subject:  srcRange,
			},
		}
	}
	ty := collection.Type()
	kty := key.Type()
	if kty == cty.DynamicPseudoType || ty == cty.DynamicPseudoType {
		return cty.DynamicVal, nil
	}

	switch {

	case ty.IsListType() || ty.IsTupleType() || ty.IsMapType():
		var wantType cty.Type
		switch {
		case ty.IsListType() || ty.IsTupleType():
			wantType = cty.Number
		case ty.IsMapType():
			wantType = cty.String
		default:
			// should never happen
			panic("don't know what key type we want")
		}

		key, keyErr := convert.Convert(key, wantType)
		if keyErr != nil {
			return cty.DynamicVal, Diagnostics{
				{
					Severity: DiagError,
					Summary:  "Invalid index",
					Detail: fmt.Sprintf(
						"The given key does not identify an element in this collection value: %s.",
						keyErr.Error(),
					),
					Subject: srcRange,
				},
			}
		}

		// Here we drop marks from HasIndex result, in order to allow basic
		// traversal of a marked list, tuple, or map in the same way we can
		// traverse a marked object
		has, _ := collection.HasIndex(key).Unmark()
		if !has.IsKnown() {
			if ty.IsTupleType() {
				return cty.DynamicVal, nil
			} else {
				return cty.UnknownVal(ty.ElementType()), nil
			}
		}
		if has.False() {
			// We have a more specialized error message for the situation of
			// using a fractional number to index into a sequence, because
			// that will tend to happen if the user is trying to use division
			// to calculate an index and not realizing that HCL does float
			// division rather than integer division.
			if (ty.IsListType() || ty.IsTupleType()) && key.Type().Equals(cty.Number) {
				if key.IsKnown() && !key.IsNull() {
					bf := key.AsBigFloat()
					if _, acc := bf.Int(nil); acc != big.Exact {
						return cty.DynamicVal, Diagnostics{
							{
								Severity: DiagError,
								Summary:  "Invalid index",
								Detail:   fmt.Sprintf("The given key does not identify an element in this collection value: indexing a sequence requires a whole number, but the given index (%g) has a fractional part.", bf),
								Subject:  srcRange,
							},
						}
					}
				}
			}

			return cty.DynamicVal, Diagnostics{
				{
					Severity: DiagError,
					Summary:  "Invalid index",
					Detail:   "The given key does not identify an element in this collection value.",
					Subject:  srcRange,
				},
			}
		}

		return collection.Index(key), nil

	case ty.IsObjectType():
		key, keyErr := convert.Convert(key, cty.String)
		if keyErr != nil {
			return cty.DynamicVal, Diagnostics{
				{
					Severity: DiagError,
					Summary:  "Invalid index",
					Detail: fmt.Sprintf(
						"The given key does not identify an element in this collection value: %s.",
						keyErr.Error(),
					),
					Subject: srcRange,
				},
			}
		}
		if !collection.IsKnown() {
			return cty.DynamicVal, nil
		}
		if !key.IsKnown() {
			return cty.DynamicVal, nil
		}

		attrName := key.AsString()

		if !ty.HasAttribute(attrName) {
			return cty.DynamicVal, Diagnostics{
				{
					Severity: DiagError,
					Summary:  "Invalid index",
					Detail:   "The given key does not identify an element in this collection value.",
					Subject:  srcRange,
				},
			}
		}

		return collection.GetAttr(attrName), nil

	default:
		return cty.DynamicVal, Diagnostics{
			{
				Severity: DiagError,
				Summary:  "Invalid index",
				Detail:   "This value does not have any indices.",
				Subject:  srcRange,
			},
		}
	}

}

// GetAttr is a helper function that performs the same operation as the
// attribute access in the HCL expression language. That is, the result is the
// same as it would be for obj.attr in a configuration expression.
//
// This is exported so that applications can access attributes in a manner
// consistent with how the language does it, including handling of null and
// unknown values, etc.
//
// Diagnostics are produced if the given combination of values is not valid.
// Therefore a pointer to a source range must be provided to use in diagnostics,
// though nil can be provided if the calling application is going to
// ignore the subject of the returned diagnostics anyway.
func GetAttr(obj cty.Value, attrName string, srcRange *Range) (cty.Value, Diagnostics) {
	if obj.IsNull() {
		return cty.DynamicVal, Diagnostics{
			{
				Severity: DiagError,
				Summary:  "Attempt to get attribute from null value",
				Detail:   "This value is null, so it does not have any attributes.",
				Subject:  srcRange,
			},
		}
	}

	ty := obj.Type()
	switch {
	case ty.IsObjectType():
		if !ty.HasAttribute(attrName) {
			return cty.DynamicVal, Diagnostics{
				{
					Severity: DiagError,
					Summary:  "Unsupported attribute",
					Detail:   fmt.Sprintf("This object does not have an attribute named %q.", attrName),
					Subject:  srcRange,
				},
			}
		}

		if !obj.IsKnown() {
			return cty.UnknownVal(ty.AttributeType(attrName)), nil
		}

		return obj.GetAttr(attrName), nil
	case ty.IsMapType():
		if !obj.IsKnown() {
			return cty.UnknownVal(ty.ElementType()), nil
		}

		idx := cty.StringVal(attrName)

		// Here we drop marks from HasIndex result, in order to allow basic
		// traversal of a marked map in the same way we can traverse a marked
		// object
		hasIndex, _ := obj.HasIndex(idx).Unmark()
		if hasIndex.False() {
			return cty.DynamicVal, Diagnostics{
				{
					Severity: DiagError,
					Summary:  "Missing map element",
					Detail:   fmt.Sprintf("This map does not have an element with the key %q.", attrName),
					Subject:  srcRange,
				},
			}
		}

		return obj.Index(idx), nil
	case ty == cty.DynamicPseudoType:
		return cty.DynamicVal, nil
	default:
		return cty.DynamicVal, Diagnostics{
			{
				Severity: DiagError,
				Summary:  "Unsupported attribute",
				Detail:   "This value does not have any attributes.",
				Subject:  srcRange,
			},
		}
	}

}

// ApplyPath is a helper function that applies a cty.Path to a value using the
// indexing and attribute access operations from HCL.
//
// This is similar to calling the path's own Apply method, but ApplyPath uses
// the more relaxed typing rules that apply to these operations in HCL, rather
// than cty's relatively-strict rules. ApplyPath is implemented in terms of
// Index and GetAttr, and so it has the same behavior for individual steps
// but will stop and return any errors returned by intermediate steps.
//
// Diagnostics are produced if the given path cannot be applied to the given
// value. Therefore a pointer to a source range must be provided to use in
// diagnostics, though nil can be provided if the calling application is going
// to ignore the subject of the returned diagnostics anyway.
func ApplyPath(val cty.Value, path cty.Path, srcRange *Range) (cty.Value, Diagnostics) {
	var diags Diagnostics

	for _, step := range path {
		var stepDiags Diagnostics
		switch ts := step.(type) {
		case cty.IndexStep:
			val, stepDiags = Index(val, ts.Key, srcRange)
		case cty.GetAttrStep:
			val, stepDiags = GetAttr(val, ts.Name, srcRange)
		default:
			// Should never happen because the above are all of the step types.
			diags = diags.Append(&Diagnostic{
				Severity: DiagError,
				Summary:  "Invalid path step",
				Detail:   fmt.Sprintf("Go type %T is not a valid path step. This is a bug in this program.", step),
				Subject:  srcRange,
			})
			return cty.DynamicVal, diags
		}

		diags = append(diags, stepDiags...)
		if stepDiags.HasErrors() {
			return cty.DynamicVal, diags
		}
	}

	return val, diags
}
