package configschema

import (
	"runtime"
	"sync"
	"unsafe"

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

var mapLabelNames = []string{"key"}

// specCache is a global cache of all the generated hcldec.Spec values for
// Blocks. This cache is used by the Block.DecoderSpec method to memoize calls
// and prevent unnecessary regeneration of the spec, especially when they are
// large and deeply nested.
// Caching these externally rather than within the struct is required because
// Blocks are used by value and copied when working with NestedBlocks, and the
// copying of the value prevents any safe synchronisation of the struct itself.
//
// While we are using the *Block pointer as the cache key, and the Block
// contents are mutable, once a Block is created it is treated as immutable for
// the duration of its life. Because a Block is a representation of a logical
// schema, which cannot change while it's being used, any modifications to the
// schema during execution would be an error.
type specCache struct {
	sync.Mutex
	specs map[uintptr]hcldec.Spec
}

var decoderSpecCache = specCache{
	specs: map[uintptr]hcldec.Spec{},
}

// get returns the Spec associated with eth given Block, or nil if non is
// found.
func (s *specCache) get(b *Block) hcldec.Spec {
	s.Lock()
	defer s.Unlock()
	k := uintptr(unsafe.Pointer(b))
	return s.specs[k]
}

// set stores the given Spec as being the result of b.DecoderSpec().
func (s *specCache) set(b *Block, spec hcldec.Spec) {
	s.Lock()
	defer s.Unlock()

	// the uintptr value gets us a unique identifier for each block, without
	// tying this to the block value itself.
	k := uintptr(unsafe.Pointer(b))
	if _, ok := s.specs[k]; ok {
		return
	}

	s.specs[k] = spec

	// This must use a finalizer tied to the Block, otherwise we'll continue to
	// build up Spec values as the Blocks are recycled.
	runtime.SetFinalizer(b, s.delete)
}

// delete removes the spec associated with the given Block.
func (s *specCache) delete(b *Block) {
	s.Lock()
	defer s.Unlock()

	k := uintptr(unsafe.Pointer(b))
	delete(s.specs, k)
}

// DecoderSpec returns a hcldec.Spec that can be used to decode a HCL Body
// using the facilities in the hcldec package.
//
// The returned specification is guaranteed to return a value of the same type
// returned by method ImpliedType, but it may contain null values if any of the
// block attributes are defined as optional and/or computed respectively.
func (b *Block) DecoderSpec() hcldec.Spec {
	ret := hcldec.ObjectSpec{}
	if b == nil {
		return ret
	}

	if spec := decoderSpecCache.get(b); spec != nil {
		return spec
	}

	for name, attrS := range b.Attributes {
		ret[name] = attrS.decoderSpec(name)
	}

	for name, blockS := range b.BlockTypes {
		if _, exists := ret[name]; exists {
			// This indicates an invalid schema, since it's not valid to
			// define both an attribute and a block type of the same name.
			// However, we don't raise this here since it's checked by
			// InternalValidate.
			continue
		}

		childSpec := blockS.Block.DecoderSpec()

		switch blockS.Nesting {
		case NestingSingle, NestingGroup:
			ret[name] = &hcldec.BlockSpec{
				TypeName: name,
				Nested:   childSpec,
				Required: blockS.MinItems == 1,
			}
			if blockS.Nesting == NestingGroup {
				ret[name] = &hcldec.DefaultSpec{
					Primary: ret[name],
					Default: &hcldec.LiteralSpec{
						Value: blockS.EmptyValue(),
					},
				}
			}
		case NestingList:
			// We prefer to use a list where possible, since it makes our
			// implied type more complete, but if there are any
			// dynamically-typed attributes inside we must use a tuple
			// instead, at the expense of our type then not being predictable.
			if blockS.Block.ImpliedType().HasDynamicTypes() {
				ret[name] = &hcldec.BlockTupleSpec{
					TypeName: name,
					Nested:   childSpec,
					MinItems: blockS.MinItems,
					MaxItems: blockS.MaxItems,
				}
			} else {
				ret[name] = &hcldec.BlockListSpec{
					TypeName: name,
					Nested:   childSpec,
					MinItems: blockS.MinItems,
					MaxItems: blockS.MaxItems,
				}
			}
		case NestingSet:
			// We forbid dynamically-typed attributes inside NestingSet in
			// InternalValidate, so we don't do anything special to handle
			// that here. (There is no set analog to tuple and object types,
			// because cty's set implementation depends on knowing the static
			// type in order to properly compute its internal hashes.)
			ret[name] = &hcldec.BlockSetSpec{
				TypeName: name,
				Nested:   childSpec,
				MinItems: blockS.MinItems,
				MaxItems: blockS.MaxItems,
			}
		case NestingMap:
			// We prefer to use a list where possible, since it makes our
			// implied type more complete, but if there are any
			// dynamically-typed attributes inside we must use a tuple
			// instead, at the expense of our type then not being predictable.
			if blockS.Block.ImpliedType().HasDynamicTypes() {
				ret[name] = &hcldec.BlockObjectSpec{
					TypeName:   name,
					Nested:     childSpec,
					LabelNames: mapLabelNames,
				}
			} else {
				ret[name] = &hcldec.BlockMapSpec{
					TypeName:   name,
					Nested:     childSpec,
					LabelNames: mapLabelNames,
				}
			}
		default:
			// Invalid nesting type is just ignored. It's checked by
			// InternalValidate.
			continue
		}
	}

	decoderSpecCache.set(b, ret)
	return ret
}

func (a *Attribute) decoderSpec(name string) hcldec.Spec {
	ret := &hcldec.AttrSpec{Name: name}
	if a == nil {
		return ret
	}

	if a.NestedType != nil {
		// FIXME: a panic() is a bad UX. Fix this, probably by extending
		// InternalValidate() to check Attribute schemas as well and calling it
		// when we get the schema from the provider in Context().
		if a.Type != cty.NilType {
			panic("Invalid attribute schema: NestedType and Type cannot both be set. This is a bug in the provider.")
		}

		ty := a.NestedType.ImpliedType()
		ret.Type = ty
		ret.Required = a.Required || a.NestedType.MinItems > 0
		return ret
	}

	ret.Type = a.Type
	ret.Required = a.Required
	return ret
}

// listOptionalAttrsFromObject is a helper function which does *not* recurse
// into NestedType Attributes, because the optional types for each of those will
// belong to their own cty.Object definitions. It is used in other functions
// which themselves handle that recursion.
func listOptionalAttrsFromObject(o *Object) []string {
	var ret []string
	for name, attr := range o.Attributes {
		if attr.Optional == true {
			ret = append(ret, name)
		}
	}
	return ret
}
