package runtime

import (
	"reflect"
	goruntime "runtime"
)

// stdMarshalerPackages are the packages of the standard library which have types with marshalers or unmarshalers,
// by their paths, which a package of another module can't have: the go command resolves the path of a package of
// the standard library to it, and reports the import as ambiguous when a module has the same path, and GOPATH
// mode finds the package of GOROOT first. A path is here only from the release of Go which has the package ( see
// std_package_go127.go ): a program built with an earlier release may have a package of its own at that path.
// The packages of encoding/json are not here: json.RawMessage and jsontext.Value hold the JSON of the user.
var stdMarshalerPackages = map[string]bool{
	"crypto/x509": true,
	"log/slog":    true,
	"math/big":    true,
	"net":         true,
	"net/netip":   true,
	"regexp":      true,
	"time":        true,
}

// IsStdMarshalerType is whether the type, or the type it points to, is a named type of a package of the standard
// library whose marshalers are trusted: what MarshalJSON returns is valid and compact, AppendText appends what
// MarshalText returns, and UnmarshalJSON and UnmarshalText keep nothing of the bytes they are given.
func IsStdMarshalerType(typ reflect.Type) bool {
	if typ.Kind() == reflect.Pointer && typ.Name() == "" {
		typ = typ.Elem()
	}
	return typ.Name() != "" && stdMarshalerPackages[typ.PkgPath()]
}

// PromotedStdMethod reports whether the method of the type which the lookup finds, or the one of the type it points to,
// is the method of a type of the standard library whose marshalers are trusted ( see IsStdMarshalerType ), which the
// type has by embedding it and doesn't declare itself: a call of the method then runs the method of that type, on the
// embedded value. It returns that type and, if the embedded value is in the value of the type ( embedded by value at
// every depth ), its offset in it, and whether it is so ( inline ), and whether the method is such a method. A type
// which declares the method, or which has it from more than one embedded type at one depth, is not such a type.
func PromotedStdMethod(typ reflect.Type, method MethodLookup) (reflect.Type, uintptr, bool, bool) {
	if typ.Kind() == reflect.Pointer && typ.Name() == "" {
		typ = typ.Elem()
	}
	if typ.Kind() != reflect.Struct || !isPromotedMethod(typ, method) {
		return nil, 0, false, false
	}
	var field reflect.StructField
	found := false
	for i := 0; i < typ.NumField(); i++ {
		f := typ.Field(i)
		if f.Anonymous && hasMethod(f.Type, method) {
			if found {
				// the depth of the method is not looked for: it is not trusted.
				return nil, 0, false, false
			}
			field, found = f, true
		}
	}
	if !found {
		return nil, 0, false, false
	}
	if IsStdMarshalerType(field.Type) {
		return field.Type, field.Offset, field.Type.Kind() != reflect.Pointer, true
	}
	origin, offset, inline, ok := PromotedStdMethod(field.Type, method)
	return origin, field.Offset + offset, inline && field.Type.Kind() != reflect.Pointer, ok
}

// hasMethod is whether the type, or the pointer to it, has the method.
func hasMethod(typ reflect.Type, method MethodLookup) bool {
	if typ.Kind() != reflect.Pointer {
		typ = reflect.PointerTo(typ)
	}
	_, ok := method(typ)
	return ok
}

// isPromotedMethod is whether the method of the struct type which the lookup finds is promoted from an embedded field:
// the method of the type and the one of the pointer to it, those which it has, are the wrappers which the compiler
// makes for a promoted method. A declared method is not a wrapper, although the method of the pointer to the type of a
// method declared on the value is.
func isPromotedMethod(typ reflect.Type, method MethodLookup) bool {
	has := false
	for _, holder := range []reflect.Type{typ, reflect.PointerTo(typ)} {
		m, ok := method(holder)
		if !ok {
			continue
		}
		if !isGeneratedCode(m.Func.Pointer()) {
			return false
		}
		has = true
	}
	return has
}

// generatedFile is the file of the code which the compiler makes, as runtime.Func.FileLine reports it.
const generatedFile = "<autogenerated>"

// isGeneratedCode is whether the code is made by the compiler, as the wrapper of a promoted method is: the runtime
// has no other way to tell it than the file of the code, which the compiler names generatedFile ( this is the
// text of the runtime, not one of this package ). A code in any other file, or of no function, is not: its method
// is then called and its output checked.
func isGeneratedCode(pc uintptr) bool {
	f := goruntime.FuncForPC(pc)
	if f == nil {
		return false
	}
	file, _ := f.FileLine(pc)
	return file == generatedFile
}
