package pipewire

import (
	"errors"
	"fmt"
	"maps"
	"slices"
	"strconv"
	"syscall"
	"time"
)

/* pipewire/core.h */

const (
	PW_TYPE_INTERFACE_Core     = PW_TYPE_INFO_INTERFACE_BASE + "Core"
	PW_TYPE_INTERFACE_Registry = PW_TYPE_INFO_INTERFACE_BASE + "Registry"
	PW_CORE_PERM_MASK          = PW_PERM_R | PW_PERM_X | PW_PERM_M
	PW_VERSION_CORE            = 4
	PW_VERSION_REGISTRY        = 3

	PW_DEFAULT_REMOTE = "pipewire-0"
	PW_ID_CORE        = 0
	PW_ID_ANY         = Word(0xffffffff)
)

const (
	PW_CORE_CHANGE_MASK_PROPS = 1 << iota

	PW_CORE_CHANGE_MASK_ALL = 1<<iota - 1
)

const (
	PW_CORE_EVENT_INFO = iota
	PW_CORE_EVENT_DONE
	PW_CORE_EVENT_PING
	PW_CORE_EVENT_ERROR
	PW_CORE_EVENT_REMOVE_ID
	PW_CORE_EVENT_BOUND_ID
	PW_CORE_EVENT_ADD_MEM
	PW_CORE_EVENT_REMOVE_MEM
	PW_CORE_EVENT_BOUND_PROPS

	PW_CORE_EVENT_NUM
	PW_VERSION_CORE_EVENTS = 1
)

const (
	PW_CORE_METHOD_ADD_LISTENER = iota

	PW_CORE_METHOD_HELLO
	PW_CORE_METHOD_SYNC
	PW_CORE_METHOD_PONG
	PW_CORE_METHOD_ERROR
	PW_CORE_METHOD_GET_REGISTRY
	PW_CORE_METHOD_CREATE_OBJECT
	PW_CORE_METHOD_DESTROY

	PW_CORE_METHOD_NUM
	PW_VERSION_CORE_METHODS = 0
)

const (
	PW_REGISTRY_EVENT_GLOBAL = iota
	PW_REGISTRY_EVENT_GLOBAL_REMOVE

	PW_REGISTRY_EVENT_NUM
	PW_VERSION_REGISTRY_EVENTS = 0
)

const (
	PW_REGISTRY_METHOD_ADD_LISTENER = iota

	PW_REGISTRY_METHOD_BIND
	PW_REGISTRY_METHOD_DESTROY

	PW_REGISTRY_METHOD_NUM
	PW_VERSION_REGISTRY_METHODS = 0
)

const (
	FOOTER_CORE_OPCODE_GENERATION = iota

	FOOTER_CORE_OPCODE_LAST
)

// The FooterCoreGeneration indicates to the client what is the current
// registry generation number of the Context on the server side.
//
// The server shall include this footer in the next message it sends that
// follows the increment of the registry generation number.
type FooterCoreGeneration struct {
	RegistryGeneration Long `json:"registry_generation"`
}

// Size satisfies [KnownSize] with a constant value.
func (fcg FooterCoreGeneration) Size() Word {
	return SizePrefix +
		Size(SizeLong)
}

// The FooterClientGeneration indicates to the server what is the last
// registry generation number the client has processed.
//
// The client shall include this footer in the next message it sends,
// after it has processed an incoming message whose footer includes a
// registry generation update.
type FooterClientGeneration struct {
	ClientGeneration Long `json:"client_generation"`
}

// Size satisfies [KnownSize] with a constant value.
func (fcg FooterClientGeneration) Size() Word {
	return SizePrefix +
		Size(SizeLong)
}

// A CoreInfo event is emitted by the server upon connection
// with the more information about the server.
type CoreInfo struct {
	// The id of the server (PW_ID_CORE).
	ID Int `json:"id"`
	// A unique cookie for this server.
	Cookie Int `json:"cookie"`
	// The name of the user running the server.
	UserName String `json:"user_name"`
	// The name of the host running the server.
	HostName String `json:"host_name"`
	// A version string of the server.
	Version String `json:"version"`
	// The name of the server.
	Name String `json:"name"`
	// A set of bits with changes to the info.
	ChangeMask Long `json:"change_mask"`
	// Optional key/value properties, valid when change_mask has PW_CORE_CHANGE_MASK_PROPS.
	Properties *SPADict `json:"props"`
}

// Opcode satisfies [Message] with a constant value.
func (c *CoreInfo) Opcode() byte { return PW_CORE_EVENT_INFO }

// FileCount satisfies [Message] with a constant value.
func (c *CoreInfo) FileCount() Int { return 0 }

// Size satisfies [KnownSize] with a value computed at runtime.
func (c *CoreInfo) Size() Word {
	return SizePrefix +
		Size(SizeInt) +
		Size(SizeInt) +
		SizeString[Word](c.UserName) +
		SizeString[Word](c.HostName) +
		SizeString[Word](c.Version) +
		SizeString[Word](c.Name) +
		Size(SizeLong) +
		c.Properties.Size()
}

// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *CoreInfo) MarshalBinary() ([]byte, error) { return Marshal(c) }

// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CoreInfo) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }

// The CoreDone event is emitted as a result of a client Sync method.
type CoreDone struct {
	// Passed from [CoreSync.ID].
	ID Int `json:"id"`
	// Passed from [CoreSync.Sequence].
	Sequence Int `json:"seq"`
}

// Opcode satisfies [Message] with a constant value.
func (c *CoreDone) Opcode() byte { return PW_CORE_EVENT_DONE }

// FileCount satisfies [Message] with a constant value.
func (c *CoreDone) FileCount() Int { return 0 }

// Size satisfies [KnownSize] with a constant value.
func (c *CoreDone) Size() Word { return SizePrefix + Size(SizeInt) + Size(SizeInt) }

// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *CoreDone) MarshalBinary() ([]byte, error) { return Marshal(c) }

// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CoreDone) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }

// The CorePing event is emitted by the server when it wants to check if a client is
// alive or ensure that it has processed the previous events.
type CorePing struct {
	// The object id to ping.
	ID Int `json:"id"`
	// Usually automatically generated.
	// The client should pass this in the Pong method reply.
	Sequence Int `json:"seq"`
}

// Opcode satisfies [Message] with a constant value.
func (c *CorePing) Opcode() byte { return PW_CORE_EVENT_PING }

// FileCount satisfies [Message] with a constant value.
func (c *CorePing) FileCount() Int { return 0 }

// Size satisfies [KnownSize] with a constant value.
func (c *CorePing) Size() Word { return SizePrefix + Size(SizeInt) + Size(SizeInt) }

// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *CorePing) MarshalBinary() ([]byte, error) { return Marshal(c) }

// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CorePing) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }

// The CoreError can be emitted by both the client and the server.
//
// When emitted by the server, the error event is sent out when a fatal
// (non-recoverable) error has occurred. The id argument is the proxy
// object where the error occurred, most often in response to a request
// to that object. The message is a brief description of the error, for
// (debugging) convenience.
//
// When emitted by the client, it indicates an error occurred in an
// object on the client.
type CoreError struct {
	// The id of the resource (proxy if emitted by the client) that is in error.
	ID Int `json:"id"`
	// A seq number from the failing request (if any).
	Sequence Int `json:"seq"`
	// A negative errno style error code.
	Result Int `json:"res"`
	// An error message.
	Message String `json:"message"`
}

// FileCount satisfies [Message] with a constant value.
func (c *CoreError) FileCount() Int { return 0 }

func (c *CoreError) Error() string {
	return "received Core::Error on" +
		" id " + strconv.Itoa(int(c.ID)) +
		" seq " + strconv.Itoa(int(c.Sequence)) +
		" res " + strconv.Itoa(int(c.Result)) +
		": " + c.Message
}

// Size satisfies [KnownSize] with a value computed at runtime.
func (c *CoreError) Size() Word {
	return SizePrefix +
		Size(SizeInt) +
		Size(SizeInt) +
		Size(SizeInt) +
		SizeString[Word](c.Message)
}

// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *CoreError) MarshalBinary() ([]byte, error) { return Marshal(c) }

// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CoreError) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }

// CoreErrorMethod is [CoreError] as a method [Message].
type CoreErrorMethod struct{ CoreError }

// Opcode satisfies [Message] with a constant value.
func (c *CoreErrorMethod) Opcode() byte { return PW_CORE_METHOD_ERROR }

// CoreErrorEvent is [CoreError] as an event [Message].
type CoreErrorEvent struct{ CoreError }

// Opcode satisfies [Message] with a constant value.
func (c *CoreErrorEvent) Opcode() byte { return PW_CORE_EVENT_ERROR }

// The CoreRemoveId event is used internally by the object ID management logic.
//
// When a client deletes an object, the server will send this event to acknowledge
// that it has seen the delete request. When the client receives this event, it
// will know that it can safely reuse the object ID.
type CoreRemoveId struct {
	// A proxy id that was removed.
	ID Int `json:"id"`
}

// Opcode satisfies [Message] with a constant value.
func (c *CoreRemoveId) Opcode() byte { return PW_CORE_EVENT_REMOVE_ID }

// FileCount satisfies [Message] with a constant value.
func (c *CoreRemoveId) FileCount() Int { return 0 }

// Size satisfies [KnownSize] with a constant value.
func (c *CoreRemoveId) Size() Word { return SizePrefix + Size(SizeInt) }

// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *CoreRemoveId) MarshalBinary() ([]byte, error) { return Marshal(c) }

// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CoreRemoveId) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }

// The CoreBoundProps event is emitted when a local object ID is bound to a global ID.
// It is emitted before the global becomes visible in the registry.
type CoreBoundProps struct {
	// A proxy id.
	ID Int `json:"id"`
	// The global_id as it will appear in the registry.
	GlobalID Int `json:"global_id"`
	// The properties of the global.
	Properties *SPADict `json:"props"`
}

// Opcode satisfies [Message] with a constant value.
func (c *CoreBoundProps) Opcode() byte { return PW_CORE_EVENT_BOUND_PROPS }

// FileCount satisfies [Message] with a constant value.
func (c *CoreBoundProps) FileCount() Int { return 0 }

// Size satisfies [KnownSize] with a value computed at runtime.
func (c *CoreBoundProps) Size() Word {
	return SizePrefix +
		Size(SizeInt) +
		Size(SizeInt) +
		c.Properties.Size()
}

// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *CoreBoundProps) MarshalBinary() ([]byte, error) { return Marshal(c) }

// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CoreBoundProps) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }

// ErrBadBoundProps is returned when a [CoreBoundProps] event targeting a proxy
// that should never be targeted is received and processed.
var ErrBadBoundProps = errors.New("attempting to store bound props on a proxy that should never be targeted")

// noAck is embedded by proxies that are never targeted by [CoreBoundProps].
type noAck struct{}

// setBoundProps should never be called as this proxy should never be targeted by [CoreBoundProps].
func (noAck) setBoundProps(*CoreBoundProps) error { panic(ErrBadBoundProps) }

// ErrBadRemove is returned when a [CoreRemoveId] event targeting a proxy
// that should never be targeted is received and processed.
var ErrBadRemove = errors.New("attempting to remove a proxy that should never be targeted")

// noRemove is embedded by proxies that are never targeted by [CoreRemoveId].
type noRemove struct{}

// remove should never be called as this proxy should never be targeted by [CoreRemoveId].
func (noRemove) remove() error { panic(ErrBadRemove) }

// ErrInvalidRemove is returned when a proxy is somehow removed twice. This is only reached for
// an implementation error as the proxy struct should no longer be reachable after the first call.
var ErrInvalidRemove = errors.New("attempting to remove an already freed proxy")

// removable is embedded by proxies that can be targeted by [CoreRemoveId] and requires no cleanup.
type removable bool

// remove checks against removal of a freed proxy and marks the proxy as removed.
func (s *removable) remove() error {
	if *s {
		panic(ErrInvalidRemove)
	}
	*s = true
	return nil
}

// ErrProxyDestroyed is returned when attempting to use a proxy method when the underlying
// proxy has already been targeted by a [CoreRemoveId] event.
var ErrProxyDestroyed = errors.New("underlying proxy has been removed")

// checkDestroy returns [ErrProxyDestroyed] if the current proxy has been destroyed.
// Must be called at the beginning of any exported method of a proxy embedding removable.
func (s *removable) checkDestroy() error {
	if *s {
		// not fatal: the caller is allowed to recover from this and allocate a new proxy
		return ErrProxyDestroyed
	}
	return nil
}

// mustCheckDestroy calls checkDestroy and panics if a non-nil error is returned.
// This is useful for non-exported methods as they should become unreachable.
func (s *removable) mustCheckDestroy() {
	if err := s.checkDestroy(); err != nil {
		panic(err)
	}
}

// destructible is embedded by proxies that can be targeted by the [CoreRemoveId] event and the
// [CoreDestroy] method and requires no cleanup. destructible purposefully does not override
// removable.mustCheckDestroy because it is used by unexported methods called during event handling
// and are exempt from the destruction check.
type destructible struct {
	destroyed bool

	removable
}

// checkDestroy overrides removable.checkDestroy to also check the destroyed field.
func (s *destructible) checkDestroy() error {
	if s.destroyed {
		return ErrProxyDestroyed
	}
	if err := s.removable.checkDestroy(); err != nil {
		return err
	}
	return nil
}

// destroy calls removable.checkDestroy then queues a [CoreDestroy] event if it succeeds.
func (s *destructible) destroy(ctx *Context, id Int) error {
	if err := s.checkDestroy(); err != nil {
		return err
	}
	l := len(ctx.pendingDestruction)
	ctx.pendingDestruction[id] = struct{}{}
	if len(ctx.pendingDestruction) != l+1 {
		return ErrProxyDestroyed
	}
	s.destroyed = true
	return ctx.GetCore().destroy(id)
}

// An InconsistentIdError describes an inconsistent state where the server claims an impossible
// proxy or global id. This is only generated by the [CoreBoundProps] event.
type InconsistentIdError struct {
	// Whether the inconsistent id is the global resource id.
	Global bool
	// Targeted proxy instance.
	Proxy fmt.Stringer
	// Differing ids.
	ID, ServerID Int
}

func (e *InconsistentIdError) Error() string {
	name := "proxy"
	if e.Global {
		name = "global"
	}

	return name + " id " + strconv.Itoa(int(e.ID)) + " targeting " + e.Proxy.String() +
		" inconsistent with " + strconv.Itoa(int(e.ServerID)) + " claimed by the server"
}

// CoreHello is the first message sent by a client.
type CoreHello struct {
	// The version number of the client, usually PW_VERSION_CORE.
	Version Int `json:"version"`
}

// Opcode satisfies [Message] with a constant value.
func (c *CoreHello) Opcode() byte { return PW_CORE_METHOD_HELLO }

// FileCount satisfies [Message] with a constant value.
func (c *CoreHello) FileCount() Int { return 0 }

// Size satisfies [KnownSize] with a constant value.
func (c *CoreHello) Size() Word { return SizePrefix + Size(SizeInt) }

// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *CoreHello) MarshalBinary() ([]byte, error) { return Marshal(c) }

// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CoreHello) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }

// hello queues a [CoreHello] message for the PipeWire server.
// This method should not be called directly, the [New] function queues this message.
func (core *Core) hello() error {
	return core.ctx.writeMessage(
		PW_ID_CORE,
		&CoreHello{PW_VERSION_CORE},
	)
}

const (
	// CoreSyncSequenceOffset is the offset to [Header.Sequence] to produce [CoreSync.Sequence].
	CoreSyncSequenceOffset = 0x40000000
)

// The CoreSync message will result in a Done event from the server.
// When the Done event is received, the client can be sure that all
// operations before the Sync method have been completed.
type CoreSync struct {
	// The id will be returned in the Done event.
	ID Int `json:"id"`
	// Usually generated automatically and will be returned in the Done event.
	Sequence Int `json:"seq"`
}

// Opcode satisfies [Message] with a constant value.
func (c *CoreSync) Opcode() byte { return PW_CORE_METHOD_SYNC }

// FileCount satisfies [Message] with a constant value.
func (c *CoreSync) FileCount() Int { return 0 }

// Size satisfies [KnownSize] with a constant value.
func (c *CoreSync) Size() Word { return SizePrefix + Size(SizeInt) + Size(SizeInt) }

// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *CoreSync) MarshalBinary() ([]byte, error) { return Marshal(c) }

// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CoreSync) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }

// sync queues a [CoreSync] message for the PipeWire server.
// This is not safe to use directly, callers should use Sync instead.
func (core *Core) sync(id Int) error {
	return core.ctx.writeMessage(
		PW_ID_CORE,
		&CoreSync{id, CoreSyncSequenceOffset + Int(core.ctx.sequence)},
	)
}

// ErrNotDone is returned if [Core.Sync] returns from its [Context.Roundtrip] without
// receiving a [CoreDone] event targeting the [CoreSync] event it delivered.
var ErrNotDone = errors.New("did not receive a Core::Done event targeting previously delivered Core::Sync")

const (
	// syncTimeout is the maximum duration [Core.Sync] is allowed to take before
	// receiving [CoreDone] or failing.
	syncTimeout = RoundtripTimeout + 15*time.Second
)

// Sync queues a [CoreSync] message for the PipeWire server and initiates a Roundtrip.
func (core *Core) Sync() error {
	core.done = false
	if err := core.sync(roundtripSyncID); err != nil {
		return err
	}
	deadline := time.Now().Add(syncTimeout)

	for !core.done {
		if time.Now().After(deadline) {
			return ErrNotDone
		}

		if err := core.ctx.roundtrip(); err != nil {
			return err
		}
	}

	if len(core.ctx.pendingIds) != 0 {
		core.ctx.closeReceivedFiles()
		return &ProxyFatalError{Err: UnacknowledgedProxyError(slices.Collect(maps.Keys(core.ctx.pendingIds))), ProxyErrs: core.ctx.cloneAsProxyErrors()}
	}
	if len(core.ctx.pendingDestruction) != 0 {
		core.ctx.closeReceivedFiles()
		return &ProxyFatalError{Err: UnacknowledgedProxyDestructionError(slices.Collect(maps.Keys(core.ctx.pendingDestruction))), ProxyErrs: core.ctx.cloneAsProxyErrors()}
	}
	return core.ctx.doSyncComplete()
}

// The CorePong message is sent from the client to the server when the server emits the Ping event.
type CorePong struct {
	// Copied from [CorePing.ID].
	ID Int `json:"id"`
	// Copied from [CorePing.Sequence]
	Sequence Int `json:"seq"`
}

// Opcode satisfies [Message] with a constant value.
func (c *CorePong) Opcode() byte { return PW_CORE_METHOD_PONG }

// FileCount satisfies [Message] with a constant value.
func (c *CorePong) FileCount() Int { return 0 }

// Size satisfies [KnownSize] with a constant value.
func (c *CorePong) Size() Word { return SizePrefix + Size(SizeInt) + Size(SizeInt) }

// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *CorePong) MarshalBinary() ([]byte, error) { return Marshal(c) }

// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CorePong) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }

// CoreGetRegistry is sent when a client requests to bind to the
// registry object and list the available objects on the server.
//
// Like with all bindings, first the client allocates a new proxy
// id and puts this as the new_id field. Methods and Events can
// then be sent and received on the new_id (in the message Id field).
type CoreGetRegistry struct {
	// The version of the registry interface used on the client,
	// usually PW_VERSION_REGISTRY.
	Version Int `json:"version"`
	// The id of the new proxy with the registry interface,
	// ends up as [Header.ID] in future messages.
	NewID Int `json:"new_id"`
}

// Opcode satisfies [Message] with a constant value.
func (c *CoreGetRegistry) Opcode() byte { return PW_CORE_METHOD_GET_REGISTRY }

// FileCount satisfies [Message] with a constant value.
func (c *CoreGetRegistry) FileCount() Int { return 0 }

// Size satisfies [KnownSize] with a constant value.
func (c *CoreGetRegistry) Size() Word { return SizePrefix + Size(SizeInt) + Size(SizeInt) }

// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *CoreGetRegistry) MarshalBinary() ([]byte, error) { return Marshal(c) }

// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CoreGetRegistry) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }

// GetRegistry queues a [CoreGetRegistry] message for the PipeWire server
// and returns the address of the newly allocated [Registry].
func (ctx *Context) GetRegistry() (*Registry, error) {
	registry := Registry{Objects: make(map[Int]RegistryGlobal), ctx: ctx}
	newId := ctx.newProxyId(&registry, false)
	registry.ID = newId
	return &registry, ctx.writeMessage(
		PW_ID_CORE,
		&CoreGetRegistry{PW_VERSION_REGISTRY, newId},
	)
}

// CoreCreateObject is sent when the client requests to create a
// new object from a factory of a certain type.
//
// The client allocates a new_id for the proxy. The server will
// allocate a new resource with the same new_id and from then on,
// Methods and Events will be exchanged between the new object of
// the given type.
type CoreCreateObject struct {
	// The name of a server factory object to use.
	FactoryName String `json:"factory_name"`
	// The type of the object to create, this is also the type of
	// the interface of the new_id proxy.
	Type String `json:"type"`
	// Undocumented, assumed to be the local version of the proxy.
	Version Int `json:"version"`
	// Extra properties to create the object.
	Properties *SPADict `json:"props"`
	// The proxy id of the new object.
	NewID Int `json:"new_id"`
}

// Opcode satisfies [Message] with a constant value.
func (c *CoreCreateObject) Opcode() byte { return PW_CORE_METHOD_CREATE_OBJECT }

// FileCount satisfies [Message] with a constant value.
func (c *CoreCreateObject) FileCount() Int { return 0 }

// Size satisfies [KnownSize] with a value computed at runtime.
func (c *CoreCreateObject) Size() Word {
	return SizePrefix +
		SizeString[Word](c.FactoryName) +
		SizeString[Word](c.Type) +
		Size(SizeInt) +
		c.Properties.Size() +
		Size(SizeInt)
}

// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *CoreCreateObject) MarshalBinary() ([]byte, error) { return Marshal(c) }

// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CoreCreateObject) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }

// createObject queues a [CoreCreateObject] message for the PipeWire server.
// This is not safe to use directly, callers should use typed wrapper methods on [Registry] instead.
func (core *Core) createObject(factoryName, typeName String, version Int, props SPADict, newId Int) error {
	return core.ctx.writeMessage(
		PW_ID_CORE,
		&CoreCreateObject{factoryName, typeName, version, &props, newId},
	)
}

// CoreDestroy is sent when the client requests to destroy an object.
type CoreDestroy struct {
	// The proxy id of the object to destroy.
	ID Int `json:"id"`
}

// Opcode satisfies [Message] with a constant value.
func (c *CoreDestroy) Opcode() byte { return PW_CORE_METHOD_DESTROY }

// FileCount satisfies [Message] with a constant value.
func (c *CoreDestroy) FileCount() Int { return 0 }

// Size satisfies [KnownSize] with a constant value.
func (c *CoreDestroy) Size() Word { return SizePrefix + Size(SizeInt) }

// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *CoreDestroy) MarshalBinary() ([]byte, error) { return Marshal(c) }

// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *CoreDestroy) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }

// destroy queues a [CoreDestroy] message for the PipeWire server.
// This is not safe to use directly, callers should use the exported method
// on the proxy implementation instead.
func (core *Core) destroy(id Int) error {
	return core.ctx.writeMessage(
		PW_ID_CORE,
		&CoreDestroy{id},
	)
}

// A RegistryGlobal event is emitted to notify a client about a new global object.
type RegistryGlobal struct {
	// The global id.
	ID Int `json:"id"`
	// Permission bits.
	Permissions Int `json:"permissions"`
	// The type of object.
	Type String `json:"type"`
	// The server version of the object.
	Version Int `json:"version"`
	// Extra global properties.
	Properties *SPADict `json:"props"`
}

// Opcode satisfies [Message] with a constant value.
func (c *RegistryGlobal) Opcode() byte { return PW_REGISTRY_EVENT_GLOBAL }

// FileCount satisfies [Message] with a constant value.
func (c *RegistryGlobal) FileCount() Int { return 0 }

// Size satisfies [KnownSize] with a value computed at runtime.
func (c *RegistryGlobal) Size() Word {
	return SizePrefix +
		Size(SizeInt) +
		Size(SizeInt) +
		SizeString[Word](c.Type) +
		Size(SizeInt) +
		c.Properties.Size()
}

// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *RegistryGlobal) MarshalBinary() ([]byte, error) { return Marshal(c) }

// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *RegistryGlobal) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }

// A RegistryGlobalRemove event is emitted when a global with id was removed.
type RegistryGlobalRemove struct {
	// The global id that was removed.
	ID Int `json:"id"`
}

// Opcode satisfies [Message] with a constant value.
func (c *RegistryGlobalRemove) Opcode() byte { return PW_REGISTRY_EVENT_GLOBAL_REMOVE }

// FileCount satisfies [Message] with a constant value.
func (c *RegistryGlobalRemove) FileCount() Int { return 0 }

// Size satisfies [KnownSize] with a constant value.
func (c *RegistryGlobalRemove) Size() Word {
	return SizePrefix +
		Size(SizeInt)
}

// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *RegistryGlobalRemove) MarshalBinary() ([]byte, error) { return Marshal(c) }

// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *RegistryGlobalRemove) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }

// RegistryBind is sent when the client requests to bind to the
// global object with id and use the client proxy with new_id as
// the proxy. After this call, methods can be sent to the remote
// global object and events can be received.
type RegistryBind struct {
	// The [RegistryGlobal.ID] to bind to.
	ID Int `json:"id"`
	// the [RegistryGlobal.Type] of the global id.
	Type String `json:"type"`
	// The client version of the interface for type.
	Version Int `json:"version"`
	// The client proxy id for the global object.
	NewID Int `json:"new_id"`
}

// Opcode satisfies [Message] with a constant value.
func (c *RegistryBind) Opcode() byte { return PW_REGISTRY_METHOD_BIND }

// FileCount satisfies [Message] with a constant value.
func (c *RegistryBind) FileCount() Int { return 0 }

// Size satisfies [KnownSize] with a value computed at runtime.
func (c *RegistryBind) Size() Word {
	return SizePrefix +
		Size(SizeInt) +
		SizeString[Word](c.Type) +
		Size(SizeInt) +
		Size(SizeInt)
}

// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *RegistryBind) MarshalBinary() ([]byte, error) { return Marshal(c) }

// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *RegistryBind) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }

// bind queues a [RegistryBind] message for the PipeWire server
// and returns the newly allocated proxy id.
func (registry *Registry) bind(proxy eventProxy, id, version Int) (Int, error) {
	bind := RegistryBind{
		ID:      id,
		Type:    proxy.String(),
		Version: version,
		NewID:   registry.ctx.newProxyId(proxy, true),
	}
	return bind.NewID, registry.ctx.writeMessage(
		registry.ID,
		&bind,
	)
}

// RegistryDestroy is sent to try to destroy the global object with id.
// This might fail when the client does not have permission.
type RegistryDestroy struct {
	// The global id to destroy.
	ID Int `json:"id"`
}

// Opcode satisfies [Message] with a constant value.
func (c *RegistryDestroy) Opcode() byte { return PW_REGISTRY_METHOD_DESTROY }

// FileCount satisfies [Message] with a constant value.
func (c *RegistryDestroy) FileCount() Int { return 0 }

// Size satisfies [KnownSize] with a constant value.
func (c *RegistryDestroy) Size() Word {
	return SizePrefix +
		Size(SizeInt)
}

// MarshalBinary satisfies [encoding.BinaryMarshaler] via [Marshal].
func (c *RegistryDestroy) MarshalBinary() ([]byte, error) { return Marshal(c) }

// UnmarshalBinary satisfies [encoding.BinaryUnmarshaler] via [Unmarshal].
func (c *RegistryDestroy) UnmarshalBinary(data []byte) error { return Unmarshal(data, c) }

// destroy queues a [RegistryDestroy] message for the PipeWire server.
func (registry *Registry) destroy(id Int) error {
	return registry.ctx.writeMessage(
		registry.ID,
		&RegistryDestroy{id},
	)
}

// Destroy tries to destroy the global object with id.
func (registry *Registry) Destroy(id Int) (err error) {
	asCoreError := registry.ctx.expectsCoreError(registry.ID, &err)
	if err != nil {
		return
	}
	if err = registry.destroy(id); err != nil {
		return err
	}
	if err = registry.ctx.GetCore().Sync(); err == nil {
		return nil
	}

	if coreError := asCoreError(); coreError == nil {
		return
	} else {
		switch syscall.Errno(-coreError.Result) {
		case syscall.EPERM:
			return &PermissionError{registry.ID, coreError.Message}

		default:
			return coreError
		}
	}
}

// An UnsupportedObjectTypeError is the name of a type not known by the server [Registry].
type UnsupportedObjectTypeError string

func (e UnsupportedObjectTypeError) Error() string   { return "unsupported object type " + string(e) }
func (e UnsupportedObjectTypeError) Message() string { return e.Error() }

// Core holds state of [PW_TYPE_INTERFACE_Core].
type Core struct {
	// Additional information from the server, populated or updated during [Context.Roundtrip].
	Info *CoreInfo `json:"info"`

	// Whether a [CoreDone] event was received during Sync.
	done bool

	ctx *Context

	noAck
	noRemove
}

// ErrUnexpectedDone is a [CoreDone] event with unexpected values.
var ErrUnexpectedDone = errors.New("multiple Core::Done events targeting Core::Sync")

// An UnknownProxyIdError describes an event targeting a proxy id that was never allocated.
type UnknownProxyIdError[E any] struct {
	// Offending id decoded from Data.
	Id Int
	// Event received from the server.
	Event E
}

func (e *UnknownProxyIdError[E]) Error() string {
	return "unknown proxy id " + strconv.Itoa(int(e.Id))
}

// An InvalidPingError is a [CorePing] event targeting a proxy id that was never allocated.
type InvalidPingError CorePing

func (e *InvalidPingError) Error() string {
	return "received Core::Ping seq " + strconv.Itoa(int(e.Sequence)) +
		" targeting unknown proxy id " + strconv.Itoa(int(e.ID))
}

func (core *Core) consume(opcode byte, files []int, unmarshal func(v any)) error {
	closeReceivedFiles(files...)
	switch opcode {
	case PW_CORE_EVENT_INFO:
		unmarshal(&core.Info)
		return nil

	case PW_CORE_EVENT_DONE:
		var done CoreDone
		unmarshal(&done)
		if done.ID == roundtripSyncID && done.Sequence == CoreSyncSequenceOffset+core.ctx.currentSeq() {
			if core.done {
				return ErrUnexpectedDone
			}
			core.done = true
		}

		// silently ignore non-matching events because the server sends out
		// an event with id -1 seq 0 that does not appear to correspond to
		// anything, and this behaviour is never mentioned in documentation
		return nil

	case PW_CORE_EVENT_PING:
		var ping CorePing
		unmarshal(&ping)
		if _, ok := core.ctx.proxy[ping.ID]; ok {
			core.ctx.mustWriteMessage(PW_ID_CORE, (*CorePong)(&ping))
			return nil
		} else {
			invalidPingError := InvalidPingError(ping)
			core.ctx.mustWriteMessage(PW_ID_CORE, &CoreErrorMethod{CoreError{
				ID:       PW_ID_CORE,
				Sequence: core.ctx.currentRemoteSeq(),
				Result:   -Int(syscall.EINVAL),
				Message:  invalidPingError.Error(),
			}})
			return &invalidPingError
		}

	case PW_CORE_EVENT_ERROR:
		var coreError CoreError
		unmarshal(&coreError)
		return &coreError

	case PW_CORE_EVENT_REMOVE_ID:
		var coreRemoveId CoreRemoveId
		unmarshal(&coreRemoveId)
		if proxy, ok := core.ctx.proxy[coreRemoveId.ID]; !ok {
			// this should never happen so is non-recoverable if it does
			panic(&UnknownProxyIdError[*CoreRemoveId]{Id: coreRemoveId.ID, Event: &coreRemoveId})
		} else {
			delete(core.ctx.proxy, coreRemoveId.ID)
			// not always populated so this is not checked
			delete(core.ctx.pendingDestruction, coreRemoveId.ID)
			return proxy.remove()
		}

	case PW_CORE_EVENT_BOUND_PROPS:
		var boundProps CoreBoundProps
		unmarshal(&boundProps)

		delete(core.ctx.pendingIds, boundProps.ID)
		proxy, ok := core.ctx.proxy[boundProps.ID]
		if !ok {
			return &UnknownProxyIdError[*CoreBoundProps]{Id: boundProps.ID, Event: &boundProps}
		}
		return proxy.setBoundProps(&boundProps)

	default:
		panic(&UnsupportedOpcodeError{opcode, core.String()})
	}
}

func (core *Core) String() string { return PW_TYPE_INTERFACE_Core }

// Registry holds state of [PW_TYPE_INTERFACE_Registry].
type Registry struct {
	// Proxy id as tracked by [Context].
	ID Int `json:"proxy_id"`

	// Global objects received via the [RegistryGlobal] event.
	//
	// This requires more processing before it can be used, but is not implemented
	// as it is not used by Hakurei.
	Objects map[Int]RegistryGlobal `json:"objects"`

	ctx *Context

	noAck
	noRemove
}

// A GlobalIDCollisionError describes a [RegistryGlobal] event stepping on a previous instance of itself.
type GlobalIDCollisionError struct {
	// The colliding id.
	ID Int
	// Involved events.
	Previous, Current *RegistryGlobal
}

func (e *GlobalIDCollisionError) Error() string {
	return "new Registry::Global event for " + e.Current.Type +
		" stepping on previous id " + strconv.Itoa(int(e.ID)) + " for " + e.Previous.Type
}

// An UnknownGlobalIDRemoveError describes a [RegistryGlobalRemove] event announcing the removal of
// a global id that is not yet known to [Registry] or was already deleted.
type UnknownGlobalIDRemoveError Int

func (e UnknownGlobalIDRemoveError) Error() string {
	return "Registry::GlobalRemove event targets unknown id " + strconv.Itoa(int(e))
}

func (registry *Registry) consume(opcode byte, files []int, unmarshal func(v any)) error {
	closeReceivedFiles(files...)
	switch opcode {
	case PW_REGISTRY_EVENT_GLOBAL:
		var global RegistryGlobal
		unmarshal(&global)
		if object, ok := registry.Objects[global.ID]; ok {
			// this should never happen so is non-recoverable if it does
			panic(&GlobalIDCollisionError{global.ID, &object, &global})
		}
		registry.Objects[global.ID] = global
		return nil

	case PW_REGISTRY_EVENT_GLOBAL_REMOVE:
		var globalRemove RegistryGlobalRemove
		unmarshal(&globalRemove)
		// server emits PW_CORE_EVENT_REMOVE_ID events targeting
		// affected proxies so they do not need to be handled here
		l := len(registry.Objects)
		delete(registry.Objects, globalRemove.ID)
		if len(registry.Objects) != l-1 {
			// this should never happen so is non-recoverable if it does
			panic(UnknownGlobalIDRemoveError(globalRemove.ID))
		}
		return nil

	default:
		panic(&UnsupportedOpcodeError{opcode, registry.String()})
	}
}

func (registry *Registry) String() string { return PW_TYPE_INTERFACE_Registry }
