// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package protohcl

import (
	"github.com/hashicorp/hcl/v2"
	"github.com/hashicorp/hcl/v2/hclparse"
	"github.com/zclconf/go-cty/cty/function"
	"google.golang.org/protobuf/reflect/protoreflect"
)

// UnmarshalContext provides information about the context in which we are
// unmarshalling HCL. It is primarily used for decoding Any fields based on
// surrounding information (e.g. the resource Type block).
type UnmarshalContext struct {
	// Parent context.
	Parent *UnmarshalContext

	// Name of the field that we are unmarshalling.
	Name string

	// Message is the protobuf message that we are unmarshalling into (may be nil).
	Message protoreflect.Message

	// Range of where this field was in the HCL source.
	Range hcl.Range
}

// ErrorRange returns a range that can be used in error messages.
func (ctx *UnmarshalContext) ErrorRange() hcl.Range {
	for {
		if !ctx.Range.Empty() || ctx.Parent == nil {
			return ctx.Range
		}
		ctx = ctx.Parent
	}
}

func Unmarshal(src []byte, dest protoreflect.ProtoMessage) error {
	return UnmarshalOptions{}.Unmarshal(src, dest)
}

type UnmarshalOptions struct {
	AnyTypeProvider AnyTypeProvider
	SourceFileName  string
	FieldNamer      FieldNamer
	Functions       map[string]function.Function
}

func (u UnmarshalOptions) Unmarshal(src []byte, dest protoreflect.ProtoMessage) error {
	rmsg := dest.ProtoReflect()

	file, diags := hclparse.NewParser().ParseHCL(src, u.SourceFileName)

	// error performing basic HCL parsing
	if diags.HasErrors() {
		return diags
	}

	u.clearAll(rmsg)

	if u.FieldNamer == nil {
		u.FieldNamer = textFieldNamer{}
	}

	return u.decodeMessage(
		&UnmarshalContext{Message: rmsg},
		u.bodyDecoder(file.Body),
		rmsg,
	)
}

func (u UnmarshalOptions) bodyDecoder(body hcl.Body) MessageDecoder {
	return newBodyDecoder(body, u.FieldNamer, u.Functions)
}

func (u UnmarshalOptions) decodeMessage(ctx *UnmarshalContext, decoder MessageDecoder, msg protoreflect.Message) error {
	desc := msg.Descriptor()

	if desc.FullName() == wellKnownTypeAny {
		return u.decodeAny(ctx, decoder, msg)
	}

	tracker := newOneOfTracker(u.FieldNamer)

	return decoder.EachField(FieldIterator{
		Desc: desc,
		Func: func(field *IterField) error {
			if err := tracker.markFieldAsSet(field.Desc); err != nil {
				return err
			}

			var (
				protoVal protoreflect.Value
				err      error
			)
			switch {
			case field.Val != nil:
				protoVal, err = u.decodeAttribute(
					&UnmarshalContext{
						Parent: ctx,
						Name:   field.Name,
						Range:  field.Range,
					},
					func() protoreflect.Value { return msg.NewField(field.Desc) },
					field.Desc,
					*field.Val,
					true,
				)
			case len(field.Blocks) != 0:
				protoVal, err = u.decodeBlocks(ctx, field.Blocks, msg, field.Desc)
			default:
				panic("decoder yielded no blocks or attributes")
			}
			if err != nil {
				return err
			}

			if protoVal.IsValid() {
				msg.Set(field.Desc, protoVal)
			}

			return nil
		},
	})
}

type newMessageFn func() protoreflect.Value

func (u UnmarshalOptions) clearAll(msg protoreflect.Message) {
	fields := msg.Descriptor().Fields()

	for i := 0; i < fields.Len(); i++ {
		msg.Clear(fields.Get(i))
	}
}
