// Package wayland implements Wayland security_context_v1 protocol.
package wayland

//go:generate sh -c "wayland-scanner client-header `pkg-config --variable=datarootdir wayland-protocols`/wayland-protocols/staging/security-context/security-context-v1.xml security-context-v1-protocol.h"
//go:generate sh -c "wayland-scanner private-code `pkg-config --variable=datarootdir wayland-protocols`/wayland-protocols/staging/security-context/security-context-v1.xml security-context-v1-protocol.c"

/*
#cgo linux pkg-config: --static wayland-client
#cgo freebsd openbsd LDFLAGS: -lwayland-client

#include "wayland-client-helper.h"
#include <wayland-version.h>
*/
import "C"
import (
	"errors"
	"os"
	"strings"
	"syscall"
)

const (
	// Version is the value of WAYLAND_VERSION.
	Version = C.WAYLAND_VERSION

	// Display contains the name of the server socket
	// (https://gitlab.freedesktop.org/wayland/wayland/-/blob/1.23.1/src/wayland-client.c#L1147)
	// which is concatenated with XDG_RUNTIME_DIR
	// (https://gitlab.freedesktop.org/wayland/wayland/-/blob/1.23.1/src/wayland-client.c#L1171)
	// or used as-is if absolute
	// (https://gitlab.freedesktop.org/wayland/wayland/-/blob/1.23.1/src/wayland-client.c#L1176).
	Display = "WAYLAND_DISPLAY"

	// FallbackName is used as the wayland socket name if WAYLAND_DISPLAY is unset
	// (https://gitlab.freedesktop.org/wayland/wayland/-/blob/1.23.1/src/wayland-client.c#L1149).
	FallbackName = "wayland-0"
)

type (
	// Res is the outcome of a call to [New].
	Res = C.hakurei_wayland_res

	// An Error represents a failure during [New].
	Error struct {
		// Where the failure occurred.
		Cause Res
		// Attempted pathname socket.
		Path string
		// Pathname socket to host server. Omitted for libwayland errors.
		Host string
		// Global errno value set during the fault.
		Errno error
	}
)

// withPrefix returns prefix suffixed with errno description if available.
func (e *Error) withPrefix(prefix string) string {
	if e.Errno == nil {
		return prefix
	}
	return prefix + ": " + e.Errno.Error()
}

const (
	// RSuccess is returned on a successful call.
	RSuccess Res = C.HAKUREI_WAYLAND_SUCCESS
	// RConnect is returned if wl_display_connect_to_fd failed. The global errno is set.
	RConnect Res = C.HAKUREI_WAYLAND_CONNECT
	// RListener is returned if wl_registry_add_listener failed. The global errno is set.
	RListener Res = C.HAKUREI_WAYLAND_LISTENER
	// RRoundtrip is returned if wl_display_roundtrip failed. The global errno is set.
	RRoundtrip Res = C.HAKUREI_WAYLAND_ROUNDTRIP
	// RNotAvail is returned if compositor does not implement wp_security_context_v1.
	RNotAvail Res = C.HAKUREI_WAYLAND_NOT_AVAIL
	// RSocket is returned if socket failed. The global errno is set.
	RSocket Res = C.HAKUREI_WAYLAND_SOCKET
	// RBind is returned if bind failed. The global errno is set.
	RBind Res = C.HAKUREI_WAYLAND_BIND
	// RListen is returned if listen failed. The global errno is set.
	RListen Res = C.HAKUREI_WAYLAND_LISTEN

	// RCreate is returned if ensuring pathname availability failed. Returned by [New].
	RCreate Res = C.HAKUREI_WAYLAND_CREAT
	// RHostSocket is returned if socket failed for host server. Returned by [New].
	RHostSocket Res = C.HAKUREI_WAYLAND_HOST_SOCKET
	// RHostConnect is returned if connect failed for host server. Returned by [New].
	RHostConnect Res = C.HAKUREI_WAYLAND_HOST_CONNECT

	// RCleanup is returned if cleanup fails. Returned by [SecurityContext.Close].
	RCleanup Res = C.HAKUREI_WAYLAND_CLEANUP
)

func (e *Error) Unwrap() error   { return e.Errno }
func (e *Error) Message() string { return e.Error() }
func (e *Error) Error() string {
	switch e.Cause {
	case RSuccess:
		if e.Errno == nil {
			return "success"
		}
		return e.Errno.Error()

	case RConnect:
		return e.withPrefix("wl_display_connect_to_fd failed")
	case RListener:
		return e.withPrefix("wl_registry_add_listener failed")
	case RRoundtrip:
		return e.withPrefix("wl_display_roundtrip failed")
	case RNotAvail:
		return "compositor does not implement security_context_v1"

	case RSocket:
		if e.Errno == nil {
			return "socket operation failed"
		}
		return "socket: " + e.Errno.Error()
	case RBind:
		return e.withPrefix("cannot bind " + e.Path)
	case RListen:
		return e.withPrefix("cannot listen on " + e.Path)

	case RCreate:
		if e.Errno == nil {
			return "cannot ensure wayland pathname socket"
		}
		return e.Errno.Error()
	case RHostSocket:
		return e.withPrefix("socket")
	case RHostConnect:
		return e.withPrefix("cannot connect to " + e.Host)

	case RCleanup:
		if pathError, ok := errors.AsType[*os.PathError](e.Errno); ok && pathError != nil {
			return pathError.Error()
		}

		if errno, ok := errors.AsType[syscall.Errno](e.Errno); ok && errno != 0 {
			return "cannot close wayland close_fd pipe: " + errno.Error()
		}

		return e.withPrefix("cannot hang up wayland security_context")

	default:
		return e.withPrefix("impossible outcome") /* not reached */
	}
}

// securityContextBind calls hakurei_security_context_bind.
//
// A non-nil error has concrete type [Error].
func securityContextBind(
	socketPath string,
	serverFd int,
	appID, instanceID string,
	closeFd int,
) error {
	if hasNull(socketPath) || hasNull(appID) || hasNull(instanceID) {
		return &Error{Cause: RBind, Path: socketPath, Errno: errors.New("argument contains NUL character")}
	}
	if !C.hakurei_is_valid_size_sun_path(C.size_t(len(socketPath))) {
		return &Error{Cause: RBind, Path: socketPath, Errno: errors.New("socket pathname too long")}
	}

	var e Error
	e.Cause, e.Errno = C.hakurei_security_context_bind(
		C.CString(socketPath),
		C.int(serverFd),
		C.CString(appID),
		C.CString(instanceID),
		C.int(closeFd),
	)

	if e.Cause == RSuccess {
		return nil
	}
	e.Path = socketPath
	return &e
}

// hasNull returns whether s contains the NUL character.
func hasNull(s string) bool { return strings.IndexByte(s, 0) > -1 }
