// SPDX-License-Identifier: Apache-2.0
// SPDX-FileCopyrightText: 2022 The Ebitengine Authors

// Package objc is a low-level pure Go objective-c runtime. This package is easy to use incorrectly, so it is best
// to use a wrapper that provides the functionality you need in a safer way.
package objc

import (
	"errors"
	"fmt"
	"math"
	"reflect"
	"regexp"
	"runtime"
	"unicode"
	"unsafe"

	"github.com/ebitengine/purego"
	"github.com/ebitengine/purego/internal/strings"
	"github.com/ebitengine/purego/internal/xreflect"
)

// TODO: support try/catch?
// https://stackoverflow.com/questions/7062599/example-of-how-objective-cs-try-catch-implementation-is-executed-at-runtime
var (
	objc_msgSend_fn                    uintptr
	objc_msgSend_stret_fn              uintptr
	objc_msgSend                       func(obj ID, cmd SEL, args ...any) ID
	objc_msgSendSuper2_fn              uintptr
	objc_msgSendSuper2_stret_fn        uintptr
	objc_msgSendSuper2                 func(super *objc_super, cmd SEL, args ...any) ID
	objc_getClass                      func(name string) Class
	objc_getProtocol                   func(name string) *Protocol
	objc_allocateProtocol              func(name string) *Protocol
	objc_registerProtocol              func(protocol *Protocol)
	objc_allocateClassPair             func(super Class, name string, extraBytes uintptr) Class
	objc_registerClassPair             func(class Class)
	sel_registerName                   func(name string) SEL
	class_getSuperclass                func(class Class) Class
	class_getInstanceVariable          func(class Class, name string) Ivar
	class_getInstanceSize              func(class Class) uintptr
	class_addMethod                    func(class Class, name SEL, imp IMP, types string) bool
	class_addIvar                      func(class Class, name string, size uintptr, alignment uint8, types string) bool
	class_addProtocol                  func(class Class, protocol *Protocol) bool
	ivar_getOffset                     func(ivar Ivar) uintptr
	ivar_getName                       func(ivar Ivar) string
	object_getClass                    func(obj ID) Class
	object_getIvar                     func(obj ID, ivar Ivar) ID
	object_setIvar                     func(obj ID, ivar Ivar, value ID)
	protocol_getName                   func(protocol *Protocol) string
	protocol_isEqual                   func(p *Protocol, p2 *Protocol) bool
	protocol_addMethodDescription      func(p *Protocol, name SEL, types string, isRequiredMethod bool, isInstanceMethod bool)
	protocol_copyMethodDescriptionList func(p *Protocol, isRequiredMethod bool, isInstanceMethod bool, outCount *uint32) *MethodDescription
	protocol_copyProtocolList          func(p *Protocol, outCount *uint32) **Protocol
	protocol_copyPropertyList2         func(p *Protocol, outCount *uint32, isRequiredProperty, isInstanceProperty bool) *Property
	protocol_addProtocol               func(p *Protocol, p2 *Protocol)
	protocol_addProperty               func(p *Protocol, name string, attributes []PropertyAttribute, attributeCount uint32, isRequiredProperty bool, isInstanceProperty bool)
	property_getName                   func(p Property) string
	property_getAttributes             func(p Property) string

	free           func(ptr unsafe.Pointer)
	_Block_copy    func(Block) Block
	_Block_release func(Block)
)

func init() {
	objc, err := purego.Dlopen("/usr/lib/libobjc.A.dylib", purego.RTLD_GLOBAL|purego.RTLD_NOW)
	if err != nil {
		panic(fmt.Errorf("objc: %w", err))
	}
	objc_msgSend_fn, err = purego.Dlsym(objc, "objc_msgSend")
	if err != nil {
		panic(fmt.Errorf("objc: %w", err))
	}
	if runtime.GOARCH == "amd64" {
		objc_msgSend_stret_fn, err = purego.Dlsym(objc, "objc_msgSend_stret")
		if err != nil {
			panic(fmt.Errorf("objc: %w", err))
		}
		objc_msgSendSuper2_stret_fn, err = purego.Dlsym(objc, "objc_msgSendSuper2_stret")
		if err != nil {
			panic(fmt.Errorf("objc: %w", err))
		}
	}
	purego.RegisterFunc(&objc_msgSend, objc_msgSend_fn)
	objc_msgSendSuper2_fn, err = purego.Dlsym(objc, "objc_msgSendSuper2")
	if err != nil {
		panic(fmt.Errorf("objc: %w", err))
	}
	purego.RegisterFunc(&objc_msgSendSuper2, objc_msgSendSuper2_fn)
	purego.RegisterLibFunc(&object_getClass, objc, "object_getClass")
	purego.RegisterLibFunc(&objc_getClass, objc, "objc_getClass")
	purego.RegisterLibFunc(&objc_getProtocol, objc, "objc_getProtocol")
	purego.RegisterLibFunc(&objc_allocateProtocol, objc, "objc_allocateProtocol")
	purego.RegisterLibFunc(&objc_registerProtocol, objc, "objc_registerProtocol")
	purego.RegisterLibFunc(&objc_allocateClassPair, objc, "objc_allocateClassPair")
	purego.RegisterLibFunc(&objc_registerClassPair, objc, "objc_registerClassPair")
	purego.RegisterLibFunc(&sel_registerName, objc, "sel_registerName")
	purego.RegisterLibFunc(&class_getSuperclass, objc, "class_getSuperclass")
	purego.RegisterLibFunc(&class_getInstanceVariable, objc, "class_getInstanceVariable")
	purego.RegisterLibFunc(&class_addMethod, objc, "class_addMethod")
	purego.RegisterLibFunc(&class_addIvar, objc, "class_addIvar")
	purego.RegisterLibFunc(&class_addProtocol, objc, "class_addProtocol")
	purego.RegisterLibFunc(&class_getInstanceSize, objc, "class_getInstanceSize")
	purego.RegisterLibFunc(&ivar_getOffset, objc, "ivar_getOffset")
	purego.RegisterLibFunc(&ivar_getName, objc, "ivar_getName")
	purego.RegisterLibFunc(&protocol_getName, objc, "protocol_getName")
	purego.RegisterLibFunc(&protocol_isEqual, objc, "protocol_isEqual")
	purego.RegisterLibFunc(&protocol_addMethodDescription, objc, "protocol_addMethodDescription")
	purego.RegisterLibFunc(&protocol_copyMethodDescriptionList, objc, "protocol_copyMethodDescriptionList")
	purego.RegisterLibFunc(&protocol_copyProtocolList, objc, "protocol_copyProtocolList")
	purego.RegisterLibFunc(&protocol_addProtocol, objc, "protocol_addProtocol")
	purego.RegisterLibFunc(&protocol_addProperty, objc, "protocol_addProperty")
	purego.RegisterLibFunc(&protocol_copyPropertyList2, objc, "protocol_copyPropertyList2")
	purego.RegisterLibFunc(&property_getName, objc, "property_getName")
	purego.RegisterLibFunc(&property_getAttributes, objc, "property_getAttributes")
	purego.RegisterLibFunc(&object_getIvar, objc, "object_getIvar")
	purego.RegisterLibFunc(&object_setIvar, objc, "object_setIvar")
	purego.RegisterLibFunc(&free, purego.RTLD_DEFAULT, "free")

	purego.RegisterLibFunc(&_Block_copy, objc, "_Block_copy")
	purego.RegisterLibFunc(&_Block_release, objc, "_Block_release")
	theBlocksCache = newBlockCache()
}

// ID is an opaque pointer to some Objective-C object
type ID uintptr

// Class returns the class of the object.
func (id ID) Class() Class {
	return object_getClass(id)
}

// Send is a convenience method for sending messages to objects. This function takes a SEL
// instead of a string since RegisterName grabs the global Objective-C lock. It is best to cache the result
// of RegisterName.
func (id ID) Send(sel SEL, args ...any) ID {
	return objc_msgSend(id, sel, args...)
}

// GetIvar reads the value of an instance variable in an object.
func (id ID) GetIvar(ivar Ivar) ID {
	return object_getIvar(id, ivar)
}

// SetIvar sets the value of an instance variable in an object.
func (id ID) SetIvar(ivar Ivar, value ID) {
	object_setIvar(id, ivar, value)
}

// keep in sync with func.go
const maxRegAllocStructSize = 16

// Send is a convenience method for sending messages to objects that can return any type.
// This function takes a SEL instead of a string since RegisterName grabs the global Objective-C lock.
// It is best to cache the result of RegisterName.
func Send[T any](id ID, sel SEL, args ...any) T {
	var fn func(id ID, sel SEL, args ...any) T
	var zero T
	if runtime.GOARCH == "amd64" &&
		reflect.ValueOf(zero).Kind() == reflect.Struct &&
		reflect.ValueOf(zero).Type().Size() > maxRegAllocStructSize {
		purego.RegisterFunc(&fn, objc_msgSend_stret_fn)
	} else {
		purego.RegisterFunc(&fn, objc_msgSend_fn)
	}
	return fn(id, sel, args...)
}

// objc_super data structure is generated by the Objective-C compiler when it encounters the super keyword
// as the receiver of a message. It specifies the class definition of the particular superclass that should
// be messaged.
type objc_super struct {
	receiver   ID
	superClass Class
}

// SendSuper is a convenience method for sending message to object's super. This function takes a SEL
// instead of a string since RegisterName grabs the global Objective-C lock. It is best to cache the result
// of RegisterName.
func (id ID) SendSuper(sel SEL, args ...any) ID {
	super := &objc_super{
		receiver:   id,
		superClass: id.Class(),
	}
	return objc_msgSendSuper2(super, sel, args...)
}

// SendSuper is a convenience method for sending message to object's super that can return any type.
// This function takes a SEL instead of a string since RegisterName grabs the global Objective-C lock.
// It is best to cache the result of RegisterName.
func SendSuper[T any](id ID, sel SEL, args ...any) T {
	super := &objc_super{
		receiver:   id,
		superClass: id.Class(),
	}
	var fn func(objcSuper *objc_super, sel SEL, args ...any) T
	var zero T
	if runtime.GOARCH == "amd64" &&
		reflect.ValueOf(zero).Kind() == reflect.Struct &&
		reflect.ValueOf(zero).Type().Size() > maxRegAllocStructSize {
		purego.RegisterFunc(&fn, objc_msgSendSuper2_stret_fn)
	} else {
		purego.RegisterFunc(&fn, objc_msgSendSuper2_fn)
	}
	return fn(super, sel, args...)
}

// SEL is an opaque type that represents a method selector
type SEL uintptr

// RegisterName registers a method with the Objective-C runtime system, maps the method name to a selector,
// and returns the selector value. This function grabs the global Objective-c lock. It is best to cache the
// result of this function.
func RegisterName(name string) SEL {
	return sel_registerName(name)
}

// Class is an opaque type that represents an Objective-C class.
type Class uintptr

// GetClass returns the Class object for the named class, or nil if the class is not registered with the Objective-C runtime.
func GetClass(name string) Class {
	return objc_getClass(name)
}

// MethodDef represents the Go function and the selector that ObjC uses to access that function.
type MethodDef struct {
	Cmd SEL
	Fn  any
}

// IvarAttrib is the attribute that an ivar has. It affects if and which methods are automatically
// generated when creating a class with RegisterClass. See [Apple Docs] for an understanding of these attributes.
// The fields are still accessible using objc.GetIvar and objc.SetIvar regardless of the value of IvarAttrib.
//
// Take for example this Objective-C code:
//
//	@property (readwrite) float value;
//
// In Go, the functions can be accessed as follows:
//
//	var value = purego.Send[float32](id, purego.RegisterName("value"))
//	id.Send(purego.RegisterName("setValue:"), 3.46)
//
// [Apple Docs]: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjectiveC/Chapters/ocProperties.html
type IvarAttrib int

const (
	ReadOnly IvarAttrib = 1 << iota
	ReadWrite
)

// FieldDef is a definition of a field to add to an Objective-C class.
// The name of the field is what will be used to access it through the Ivar. If the type is bool
// the name cannot start with `is` since a getter will be generated with the name `isBoolName`.
// The name also cannot contain any spaces.
// The type is the Go equivalent type of the Ivar.
// Attribute determines if a getter and or setter method is generated for this field.
type FieldDef struct {
	Name      string
	Type      reflect.Type
	Attribute IvarAttrib
}

// ivarRegex checks to make sure the Ivar is correctly formatted
var ivarRegex = regexp.MustCompile("[a-z_][a-zA-Z0-9_]*")

// RegisterClass takes the name of the class to create, the superclass, a list of protocols this class
// implements, a list of fields this class has and a list of methods. It returns the created class or an error
// describing what went wrong.
func RegisterClass(name string, superClass Class, protocols []*Protocol, ivars []FieldDef, methods []MethodDef) (Class, error) {
	class := objc_allocateClassPair(superClass, name, 0)
	if class == 0 {
		return 0, fmt.Errorf("objc: failed to create class with name '%s'", name)
	}
	// Add Protocols
	for _, p := range protocols {
		if !class.AddProtocol(p) {
			return 0, fmt.Errorf("objc: couldn't add Protocol %s", protocol_getName(p))
		}
	}
	// Add exported methods based on the selectors returned from ClassDef(string) SEL
	for idx, def := range methods {
		imp, err := func() (imp IMP, err error) {
			defer func() {
				if r := recover(); r != nil {
					err = fmt.Errorf("objc: failed to create IMP: %s", r)
				}
			}()
			return NewIMP(def.Fn), nil
		}()
		if err != nil {
			return 0, fmt.Errorf("objc: couldn't add Method at index %d: %w", idx, err)
		}
		encoding, err := encodeFunc(def.Fn)
		if err != nil {
			return 0, fmt.Errorf("objc: couldn't add Method at index %d: %w", idx, err)
		}
		if !class.AddMethod(def.Cmd, imp, encoding) {
			return 0, fmt.Errorf("objc: couldn't add Method at index %d", idx)
		}
	}
	// Add Ivars
	for _, instVar := range ivars {
		ivar := instVar
		if !ivarRegex.MatchString(ivar.Name) {
			return 0, fmt.Errorf("objc: Ivar must start with a lowercase letter and only contain ASCII letters and numbers: '%s'", ivar.Name)
		}
		size := ivar.Type.Size()
		alignment := uint8(math.Log2(float64(ivar.Type.Align())))
		enc, err := encodeType(ivar.Type, false)
		if err != nil {
			return 0, fmt.Errorf("objc: couldn't add Ivar %s: %w", ivar.Name, err)
		}
		if !class_addIvar(class, ivar.Name, size, alignment, enc) {
			return 0, fmt.Errorf("objc: couldn't add Ivar %s", ivar.Name)
		}
		offset := class.InstanceVariable(ivar.Name).Offset()
		switch ivar.Attribute {
		case ReadWrite:
			ty := reflect.FuncOf(
				[]reflect.Type{
					reflect.TypeOf(ID(0)), reflect.TypeOf(SEL(0)), ivar.Type,
				},
				nil, false,
			)
			var encoding string
			if encoding, err = encodeFunc(reflect.New(ty).Elem().Interface()); err != nil {
				return 0, fmt.Errorf("objc: failed to create read method for '%s': %w", ivar.Name, err)
			}
			val := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) {
				// on entry the first and second arguments are ID and SEL followed by the value
				if len(args) != 3 {
					panic(fmt.Sprintf("objc: incorrect number of args. expected 3 got %d", len(args)))
				}
				// The following reflect code does the equivalent of this:
				//
				//	((*struct {
				//		Padding [offset]byte
				//		Value int
				//	})(unsafe.Pointer(args[0].Interface().(ID)))).v = 123
				//
				// However, since the type of the variable is unknown reflection is used to actually assign the value
				id, ok := xreflect.TypeAssert[ID](args[0])
				if !ok {
					panic(fmt.Sprintf("objc: id argument is not a ID but %s", args[0].Type().String()))
				}
				ptr := *(*unsafe.Pointer)(unsafe.Pointer(&id)) // circumvent go vet
				reflect.NewAt(ivar.Type, unsafe.Add(ptr, offset)).Elem().Set(args[2])
				return nil
			}).Interface()
			// this code only works for ascii but that shouldn't be a problem
			selector := "set" + string(unicode.ToUpper(rune(ivar.Name[0]))) + ivar.Name[1:] + ":\x00"
			class.AddMethod(RegisterName(selector), NewIMP(val), encoding)
			fallthrough // also implement the read method
		case ReadOnly:
			ty := reflect.FuncOf(
				[]reflect.Type{
					reflect.TypeOf(ID(0)), reflect.TypeOf(SEL(0)),
				},
				[]reflect.Type{ivar.Type}, false,
			)
			var encoding string
			if encoding, err = encodeFunc(reflect.New(ty).Elem().Interface()); err != nil {
				return 0, fmt.Errorf("objc: failed to create read method for '%s': %w", ivar.Name, err)
			}
			val := reflect.MakeFunc(ty, func(args []reflect.Value) (results []reflect.Value) {
				// on entry the first and second arguments are ID and SEL
				if len(args) != 2 {
					panic(fmt.Sprintf("objc: incorrect number of args. expected 2 got %d", len(args)))
				}
				id, ok := xreflect.TypeAssert[ID](args[0])
				if !ok {
					panic(fmt.Sprintf("objc: id argument is not a ID but %s", args[0].Type().String()))
				}
				ptr := *(*unsafe.Pointer)(unsafe.Pointer(&id)) // circumvent go vet
				// the variable is located at an offset from the id
				return []reflect.Value{reflect.NewAt(ivar.Type, unsafe.Add(ptr, offset)).Elem()}
			}).Interface()
			if ivar.Type.Kind() == reflect.Bool {
				// this code only works for ascii but that shouldn't be a problem
				ivar.Name = "is" + string(unicode.ToUpper(rune(ivar.Name[0]))) + ivar.Name[1:]
			}
			class.AddMethod(RegisterName(ivar.Name), NewIMP(val), encoding)
		default:
			return 0, fmt.Errorf("objc: unknown Ivar Attribute (%d)", ivar.Attribute)
		}
	}
	objc_registerClassPair(class)
	return class, nil
}

const (
	encId          = "@"
	encClass       = "#"
	encSelector    = ":"
	encChar        = "c"
	encUChar       = "C"
	encShort       = "s"
	encUShort      = "S"
	encInt         = "i"
	encUInt        = "I"
	encLong        = "l"
	encULong       = "L"
	encFloat       = "f"
	encDouble      = "d"
	encBool        = "B"
	encVoid        = "v"
	encPtr         = "^"
	encCharPtr     = "*"
	encStructBegin = "{"
	encStructEnd   = "}"
	encUnsafePtr   = "^v"
)

// encodeType returns a string representing a type as if it was given to @encode(typ)
// Source: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html#//apple_ref/doc/uid/TP40008048-CH100
func encodeType(typ reflect.Type, insidePtr bool) (string, error) {
	switch typ {
	case reflect.TypeOf(Class(0)):
		return encClass, nil
	case reflect.TypeOf(ID(0)), reflect.TypeOf(Block(0)):
		return encId, nil
	case reflect.TypeOf(SEL(0)):
		return encSelector, nil
	}

	kind := typ.Kind()
	switch kind {
	case reflect.Bool:
		return encBool, nil
	case reflect.Int:
		return encLong, nil
	case reflect.Int8:
		return encChar, nil
	case reflect.Int16:
		return encShort, nil
	case reflect.Int32:
		return encInt, nil
	case reflect.Int64:
		return encULong, nil
	case reflect.Uint:
		return encULong, nil
	case reflect.Uint8:
		return encUChar, nil
	case reflect.Uint16:
		return encUShort, nil
	case reflect.Uint32:
		return encUInt, nil
	case reflect.Uint64:
		return encULong, nil
	case reflect.Uintptr:
		return encPtr, nil
	case reflect.Float32:
		return encFloat, nil
	case reflect.Float64:
		return encDouble, nil
	case reflect.Ptr:
		enc, err := encodeType(typ.Elem(), true)
		return encPtr + enc, err
	case reflect.Struct:
		if insidePtr {
			return encStructBegin + typ.Name() + encStructEnd, nil
		}
		encoding := encStructBegin
		encoding += typ.Name()
		encoding += "="
		for i := 0; i < typ.NumField(); i++ {
			f := typ.Field(i)
			tmp, err := encodeType(f.Type, false)
			if err != nil {
				return "", err
			}
			encoding += tmp
		}
		encoding += encStructEnd
		return encoding, nil
	case reflect.UnsafePointer:
		return encUnsafePtr, nil
	case reflect.String:
		return encCharPtr, nil
	}

	return "", errors.New(fmt.Sprintf("unhandled/invalid kind %v typed %v", kind, typ))
}

// encodeFunc returns a functions type as if it was given to @encode(fn)
func encodeFunc(fn any) (string, error) {
	typ := reflect.TypeOf(fn)
	if typ.Kind() != reflect.Func {
		return "", errors.New("not a func")
	}

	encoding := ""
	switch typ.NumOut() {
	case 0:
		encoding += encVoid
	case 1:
		tmp, err := encodeType(typ.Out(0), false)
		if err != nil {
			return "", err
		}
		encoding += tmp
	default:
		return "", errors.New("too many output parameters")
	}

	if typ.NumIn() < 2 {
		return "", errors.New("func doesn't take ID and SEL as its first two parameters")
	}

	encoding += encId

	for i := 1; i < typ.NumIn(); i++ {
		tmp, err := encodeType(typ.In(i), false)
		if err != nil {
			return "", err
		}
		encoding += tmp
	}
	return encoding, nil
}

// SuperClass returns the superclass of a class.
// You should usually use NSObject‘s superclass method instead of this function.
func (c Class) SuperClass() Class {
	return class_getSuperclass(c)
}

// AddMethod adds a new method to a class with a given name and implementation.
// The types argument is a string containing the mapping of parameters and return type.
// Since the function must take at least two arguments—self and _cmd, the second and third
// characters must be “@:” (the first character is the return type).
func (c Class) AddMethod(name SEL, imp IMP, types string) bool {
	return class_addMethod(c, name, imp, types)
}

// AddProtocol adds a protocol to a class.
// Returns true if the protocol was added successfully, otherwise false (for example,
// the class already conforms to that protocol).
func (c Class) AddProtocol(protocol *Protocol) bool {
	return class_addProtocol(c, protocol)
}

// InstanceSize returns the size in bytes of instances of the class or 0 if cls is nil
func (c Class) InstanceSize() uintptr {
	return class_getInstanceSize(c)
}

// InstanceVariable returns an Ivar data structure containing information about the instance variable specified by name.
func (c Class) InstanceVariable(name string) Ivar {
	return class_getInstanceVariable(c, name)
}

// Ivar an opaque type that represents an instance variable.
type Ivar uintptr

// Offset returns the offset of an instance variable that can be used to assign and read the Ivar's value.
//
// For instance variables of type ID or other object types, call Ivar and SetIvar instead
// of using this offset to access the instance variable data directly.
func (i Ivar) Offset() uintptr {
	return ivar_getOffset(i)
}

func (i Ivar) Name() string {
	return ivar_getName(i)
}

// MethodDescription holds the name and type definition of a method.
type MethodDescription struct {
	name, types uintptr
}

// Name returns the name of this method.
func (m MethodDescription) Name() string {
	return strings.GoString(m.name)
}

// Types returns the OBJC runtime encoded type description.
func (m MethodDescription) Types() string {
	return strings.GoString(m.types)
}

// PropertyAttribute contains the null-terminated Name and Value pair of a Properties internal description.
type PropertyAttribute struct {
	Name, Value *byte
}

// Property is an opaque type for Objective-C property metadata.
type Property uintptr

// Name returns the name of this property.
func (p Property) Name() string {
	return property_getName(p)
}

// Attributes returns a comma separated list of PropertyAttribute
func (p Property) Attributes() string {
	return property_getAttributes(p)
}

// Protocol is a type that declares methods that can be implemented by any class.
type Protocol [0]func()

// GetProtocol returns the protocol for the given name or nil if there is no protocol by that name.
func GetProtocol(name string) *Protocol {
	return objc_getProtocol(name)
}

// AllocateProtocol creates a new protocol in the OBJC runtime or nil if the protocol already exists.
func AllocateProtocol(name string) *Protocol {
	return objc_allocateProtocol(name)
}

// Register registers the protocol created using AllocateProtocol with the runtime. This must be done
// before it is used anywhere and can only be called once.
func (p *Protocol) Register() {
	objc_registerProtocol(p)
}

// CopyMethodDescriptionList returns a list of methods that this protocol has given it isRequiredMethod and isInstanceMethod.
func (p *Protocol) CopyMethodDescriptionList(isRequiredMethod, isInstanceMethod bool) []MethodDescription {
	count := uint32(0)
	desc := protocol_copyMethodDescriptionList(p, isRequiredMethod, isInstanceMethod, &count)
	methods := clone(unsafe.Slice(desc, count))
	free(unsafe.Pointer(desc))
	return methods
}

// CopyProtocolList returns a list of the protocols that this protocol inherits from.
func (p *Protocol) CopyProtocolList() []*Protocol {
	count := uint32(0)
	desc := protocol_copyProtocolList(p, &count)
	protocols := clone(unsafe.Slice(desc, count))
	free(unsafe.Pointer(desc))
	return protocols
}

// CopyPropertyList returns a list of properties that this protocol has given it isRequiredProperty and isInstanceProperty.
func (p *Protocol) CopyPropertyList(isRequiredProperty, isInstanceProperty bool) []Property {
	count := uint32(0)
	desc := protocol_copyPropertyList2(p, &count, isRequiredProperty, isInstanceProperty)
	protocols := clone(unsafe.Slice(desc, count))
	free(unsafe.Pointer(desc))
	return protocols
}

// Name returns the name of this protocol.
func (p *Protocol) Name() string {
	return protocol_getName(p)
}

// Equals return true if the two protocols are the same.
func (p *Protocol) Equals(p2 *Protocol) bool {
	return protocol_isEqual(p, p2)
}

// AddMethodDescription adds a method to a protocol. This can only be called between AllocateProtocol and Protocol.Register.
func (p *Protocol) AddMethodDescription(name SEL, types string, isRequiredMethod, isInstanceMethod bool) {
	protocol_addMethodDescription(p, name, types, isRequiredMethod, isInstanceMethod)
}

// AddProtocol marks the protocol as inheriting from another. This can only be called between AllocateProtocol and Protocol.Register.
func (p *Protocol) AddProtocol(protocol *Protocol) {
	protocol_addProtocol(p, protocol)
}

// AddProperty adds a property to the protocol. This can only be called between AllocateProtocol and Protocol.Register.
func (p *Protocol) AddProperty(name string, attributes []PropertyAttribute, isRequiredProperty, isInstanceProperty bool) {
	protocol_addProperty(p, name, attributes, uint32(len(attributes)), isRequiredProperty, isInstanceProperty)
}

// IMP is a function pointer that can be called by Objective-C code.
type IMP uintptr

// NewIMP takes a Go function that takes (ID, SEL) as its first two arguments.
// It returns an IMP function pointer that can be called by Objective-C code.
// The function panics if an error occurs.
// The function pointer is never deallocated.
func NewIMP(fn any) IMP {
	ty := reflect.TypeOf(fn)
	if ty.Kind() != reflect.Func {
		panic("objc: not a function")
	}
	// IMP is stricter than a normal callback
	// id (*IMP)(id, SEL, ...)
	switch {
	case ty.NumIn() < 2:
		fallthrough
	case ty.In(0) != reflect.TypeOf(ID(0)):
		fallthrough
	case ty.In(1) != reflect.TypeOf(SEL(0)):
		panic("objc: NewIMP must take a (id, SEL) as its first two arguments; got " + ty.String())
	}
	return IMP(purego.NewCallback(fn))
}

// TODO: remove and use slices.Clone when minimum version for purego is 1.21
func clone[S ~[]E, E any](s S) S {
	// Preserve nilness in case it matters.
	if s == nil {
		return nil
	}
	// Avoid s[:0:0] as it leads to unwanted liveness when cloning a
	// zero-length slice of a large array; see https://go.dev/issue/68488.
	return append(S{}, s...)
}
