package dbus

import (
	"errors"
	"fmt"
	"os"
	"reflect"
	"strings"
)

var (
	ErrMsgInvalidArg = Error{
		"org.freedesktop.DBus.Error.InvalidArgs",
		[]any{"Invalid type / number of args"},
	}
	ErrMsgNoObject = Error{
		"org.freedesktop.DBus.Error.NoSuchObject",
		[]any{"No such object"},
	}
	ErrMsgUnknownMethod = Error{
		"org.freedesktop.DBus.Error.UnknownMethod",
		[]any{"Unknown / invalid method"},
	}
	ErrMsgUnknownInterface = Error{
		"org.freedesktop.DBus.Error.UnknownInterface",
		[]any{"Object does not implement the interface"},
	}
)

func MakeNoObjectError(path ObjectPath) Error {
	return Error{
		"org.freedesktop.DBus.Error.NoSuchObject",
		[]any{fmt.Sprintf("No such object '%s'", string(path))},
	}
}

func MakeUnknownMethodError(methodName string) Error {
	return Error{
		"org.freedesktop.DBus.Error.UnknownMethod",
		[]any{fmt.Sprintf("Unknown / invalid method '%s'", methodName)},
	}
}

func MakeUnknownInterfaceError(ifaceName string) Error {
	return Error{
		"org.freedesktop.DBus.Error.UnknownInterface",
		[]any{fmt.Sprintf("Object does not implement the interface '%s'", ifaceName)},
	}
}

func MakeFailedError(err error) *Error {
	return &Error{
		"org.freedesktop.DBus.Error.Failed",
		[]any{err.Error()},
	}
}

// Sender is a type which can be used in exported methods to receive the message
// sender.
type Sender string

func computeMethodName(name string, mapping map[string]string) string {
	newname, ok := mapping[name]
	if ok {
		name = newname
	}
	return name
}

func getMethods(in any, mapping map[string]string) map[string]reflect.Value {
	if in == nil {
		return nil
	}
	methods := make(map[string]reflect.Value)
	val := reflect.ValueOf(in)
	typ := val.Type()
	for i := 0; i < typ.NumMethod(); i++ {
		methtype := typ.Method(i)
		method := val.Method(i)
		t := method.Type()
		// only track valid methods must return *Error as last arg
		// and must be exported
		if t.NumOut() == 0 ||
			t.Out(t.NumOut()-1) != reflect.TypeOf(&ErrMsgInvalidArg) ||
			methtype.PkgPath != "" {
			continue
		}
		// map names while building table
		methods[computeMethodName(methtype.Name, mapping)] = method
	}
	return methods
}

func getAllMethods(in any, mapping map[string]string) map[string]reflect.Value {
	if in == nil {
		return nil
	}
	methods := make(map[string]reflect.Value)
	val := reflect.ValueOf(in)
	typ := val.Type()
	for i := 0; i < typ.NumMethod(); i++ {
		methtype := typ.Method(i)
		method := val.Method(i)
		// map names while building table
		methods[computeMethodName(methtype.Name, mapping)] = method
	}
	return methods
}

func standardMethodArgumentDecode(m Method, sender string, msg *Message, body []any) ([]any, error) {
	pointers := make([]any, m.NumArguments())
	decode := make([]any, 0, len(body))

	for i := 0; i < m.NumArguments(); i++ {
		tp := reflect.TypeOf(m.ArgumentValue(i))
		val := reflect.New(tp)
		pointers[i] = val.Interface()
		if tp == reflect.TypeOf((*Sender)(nil)).Elem() {
			val.Elem().SetString(sender)
		} else if tp == reflect.TypeOf((*Message)(nil)).Elem() {
			val.Elem().Set(reflect.ValueOf(*msg))
		} else {
			decode = append(decode, pointers[i])
		}
	}

	if len(decode) != len(body) {
		return nil, ErrMsgInvalidArg
	}

	if err := Store(body, decode...); err != nil {
		return nil, ErrMsgInvalidArg
	}

	return pointers, nil
}

func (conn *Conn) decodeArguments(m Method, sender string, msg *Message) ([]any, error) {
	if decoder, ok := m.(ArgumentDecoder); ok {
		return decoder.DecodeArguments(conn, sender, msg, msg.Body)
	}
	return standardMethodArgumentDecode(m, sender, msg, msg.Body)
}

// handleCall handles the given method call (i.e. looks if it's one of the
// pre-implemented ones and searches for a corresponding handler if not).
func (conn *Conn) handleCall(msg *Message) {
	name := msg.Headers[FieldMember].value.(string)
	path := msg.Headers[FieldPath].value.(ObjectPath)
	ifaceName, _ := msg.Headers[FieldInterface].value.(string)
	sender, hasSender := msg.Headers[FieldSender].value.(string)
	serial := msg.serial

	if len(name) == 0 {
		conn.sendError(ErrMsgUnknownMethod, sender, serial)
	}

	if ifaceName == "org.freedesktop.DBus.Peer" {
		switch name {
		case "Ping":
			conn.sendReply(sender, serial)
		case "GetMachineId":
			conn.sendReply(sender, serial, conn.uuid)
		default:
			conn.sendError(MakeUnknownMethodError(name), sender, serial)
		}
		return
	}

	object, ok := conn.handler.LookupObject(path)
	if !ok {
		conn.sendError(MakeNoObjectError(path), sender, serial)
		return
	}

	iface, exists := object.LookupInterface(ifaceName)
	if !exists {
		conn.sendError(MakeUnknownInterfaceError(ifaceName), sender, serial)
		return
	}

	m, exists := iface.LookupMethod(name)
	if !exists {
		conn.sendError(MakeUnknownMethodError(name), sender, serial)
		return
	}
	args, err := conn.decodeArguments(m, sender, msg)
	if err != nil {
		conn.sendError(err, sender, serial)
		return
	}

	ret, err := m.Call(args...)
	if err != nil {
		conn.sendError(err, sender, serial)
		return
	}

	if msg.Flags&FlagNoReplyExpected == 0 {
		reply := new(Message)
		reply.Type = TypeMethodReply
		reply.Headers = make(map[HeaderField]Variant)
		if hasSender {
			reply.Headers[FieldDestination] = msg.Headers[FieldSender]
		}
		reply.Headers[FieldReplySerial] = MakeVariant(msg.serial)
		reply.Body = make([]any, len(ret))
		copy(reply.Body, ret)
		reply.Headers[FieldSignature] = MakeVariant(SignatureOf(reply.Body...))

		if err := conn.sendMessageAndIfClosed(reply, nil); err != nil {
			if _, ok := err.(FormatError); ok {
				fmt.Fprintf(os.Stderr, "dbus: replacing invalid reply to %s.%s on obj %s: %s\n", ifaceName, name, path, err)
			}
		}
	}
}

// Emit emits the given signal on the message bus. The name parameter must be
// formatted as "interface.member", e.g., "org.freedesktop.DBus.NameLost".
func (conn *Conn) Emit(path ObjectPath, name string, values ...any) error {
	i := strings.LastIndex(name, ".")
	if i == -1 {
		return errors.New("dbus: invalid method name")
	}
	iface := name[:i]
	member := name[i+1:]
	msg := new(Message)
	msg.Type = TypeSignal
	msg.Headers = make(map[HeaderField]Variant)
	msg.Headers[FieldInterface] = MakeVariant(iface)
	msg.Headers[FieldMember] = MakeVariant(member)
	msg.Headers[FieldPath] = MakeVariant(path)
	msg.Body = values
	if len(values) > 0 {
		msg.Headers[FieldSignature] = MakeVariant(SignatureOf(values...))
	}

	var closed bool
	err := conn.sendMessageAndIfClosed(msg, func() {
		closed = true
	})
	if closed {
		return ErrClosed
	}
	return err
}

// Export registers the given value to be exported as an object on the
// message bus.
//
// If a method call on the given path and interface is received, an exported
// method with the same name is called with v as the receiver if the
// parameters match and the last return value is of type *Error. If this
// *Error is not nil, it is sent back to the caller as an error.
// Otherwise, a method reply is sent with the other return values as its body.
//
// Any parameters with the special type Sender are set to the sender of the
// dbus message when the method is called. Parameters of this type do not
// contribute to the dbus signature of the method (i.e. the method is exposed
// as if the parameters of type Sender were not there).
//
// Similarly, any parameters with the type Message are set to the raw message
// received on the bus. Again, parameters of this type do not contribute to the
// dbus signature of the method.
//
// Every method call is executed in a new goroutine, so the method may be called
// in multiple goroutines at once.
//
// Method calls on the interface org.freedesktop.DBus.Peer will be automatically
// handled for every object.
//
// Passing nil as the first parameter will cause conn to cease handling calls on
// the given combination of path and interface.
//
// Export returns an error if path is not a valid path name.
func (conn *Conn) Export(v any, path ObjectPath, iface string) error {
	return conn.ExportWithMap(v, nil, path, iface)
}

// ExportAll registers all exported methods defined by the given object on
// the message bus.
//
// Unlike Export there is no requirement to have the last parameter as type
// *Error. If you want to be able to return error then you can append an error
// type parameter to your method signature. If the error returned is not nil,
// it is sent back to the caller as an error. Otherwise, a method reply is
// sent with the other return values as its body.
func (conn *Conn) ExportAll(v any, path ObjectPath, iface string) error {
	return conn.export(getAllMethods(v, nil), path, iface, false)
}

// ExportWithMap works exactly like Export but provides the ability to remap
// method names (e.g. export a lower-case method).
//
// The keys in the map are the real method names (exported on the struct), and
// the values are the method names to be exported on DBus.
func (conn *Conn) ExportWithMap(v any, mapping map[string]string, path ObjectPath, iface string) error {
	return conn.export(getMethods(v, mapping), path, iface, false)
}

// ExportSubtree works exactly like Export but registers the given value for
// an entire subtree rather under the root path provided.
//
// In order to make this useful, one parameter in each of the value's exported
// methods should be a Message, in which case it will contain the raw message
// (allowing one to get access to the path that caused the method to be called).
//
// Note that more specific export paths take precedence over less specific. For
// example, a method call using the ObjectPath /foo/bar/baz will call a method
// exported on /foo/bar before a method exported on /foo.
func (conn *Conn) ExportSubtree(v any, path ObjectPath, iface string) error {
	return conn.ExportSubtreeWithMap(v, nil, path, iface)
}

// ExportSubtreeWithMap works exactly like ExportSubtree but provides the
// ability to remap method names (e.g. export a lower-case method).
//
// The keys in the map are the real method names (exported on the struct), and
// the values are the method names to be exported on DBus.
func (conn *Conn) ExportSubtreeWithMap(v any, mapping map[string]string, path ObjectPath, iface string) error {
	return conn.export(getMethods(v, mapping), path, iface, true)
}

// ExportMethodTable like Export registers the given methods as an object
// on the message bus. Unlike Export the it uses a method table to define
// the object instead of a native go object.
//
// The method table is a map from method name to function closure
// representing the method. This allows an object exported on the bus to not
// necessarily be a native go object. It can be useful for generating exposed
// methods on the fly.
//
// Any non-function objects in the method table are ignored.
func (conn *Conn) ExportMethodTable(methods map[string]any, path ObjectPath, iface string) error {
	return conn.exportMethodTable(methods, path, iface, false)
}

// Like ExportSubtree, but with the same caveats as ExportMethodTable.
func (conn *Conn) ExportSubtreeMethodTable(methods map[string]any, path ObjectPath, iface string) error {
	return conn.exportMethodTable(methods, path, iface, true)
}

func (conn *Conn) exportMethodTable(methods map[string]any, path ObjectPath, iface string, includeSubtree bool) error {
	var out map[string]reflect.Value
	if methods != nil {
		out = make(map[string]reflect.Value)
		for name, method := range methods {
			rval := reflect.ValueOf(method)
			if rval.Kind() != reflect.Func {
				continue
			}
			t := rval.Type()
			// only track valid methods must return *Error as last arg
			if t.NumOut() == 0 ||
				t.Out(t.NumOut()-1) != reflect.TypeOf(&ErrMsgInvalidArg) {
				continue
			}
			out[name] = rval
		}
	}
	return conn.export(out, path, iface, includeSubtree)
}

func (conn *Conn) unexport(h *defaultHandler, path ObjectPath, iface string) error {
	if h.PathExists(path) {
		obj := h.objects[path]
		obj.DeleteInterface(iface)
		if len(obj.interfaces) == 0 {
			h.DeleteObject(path)
		}
	}
	return nil
}

// export is the worker function for all exports/registrations.
func (conn *Conn) export(methods map[string]reflect.Value, path ObjectPath, iface string, includeSubtree bool) error {
	h, ok := conn.handler.(*defaultHandler)
	if !ok {
		return fmt.Errorf(
			`dbus: export only allowed on the default handler. Received: %T"`,
			conn.handler)
	}

	if !path.IsValid() {
		return fmt.Errorf(`dbus: Invalid path name: "%s"`, path)
	}

	// Remove a previous export if the interface is nil
	if methods == nil {
		return conn.unexport(h, path, iface)
	}

	// If this is the first handler for this path, make a new map to hold all
	// handlers for this path.
	if !h.PathExists(path) {
		h.AddObject(path, newExportedObject())
	}

	exportedMethods := make(map[string]Method)
	for name, method := range methods {
		exportedMethods[name] = exportedMethod{method}
	}

	// Finally, save this handler
	obj := h.objects[path]
	obj.AddInterface(iface, newExportedIntf(exportedMethods, includeSubtree))

	return nil
}

// ReleaseName calls org.freedesktop.DBus.ReleaseName and awaits a response.
func (conn *Conn) ReleaseName(name string) (ReleaseNameReply, error) {
	var r uint32
	err := conn.busObj.Call("org.freedesktop.DBus.ReleaseName", 0, name).Store(&r)
	if err != nil {
		return 0, err
	}
	return ReleaseNameReply(r), nil
}

// RequestName calls org.freedesktop.DBus.RequestName and awaits a response.
func (conn *Conn) RequestName(name string, flags RequestNameFlags) (RequestNameReply, error) {
	var r uint32
	err := conn.busObj.Call("org.freedesktop.DBus.RequestName", 0, name, flags).Store(&r)
	if err != nil {
		return 0, err
	}
	return RequestNameReply(r), nil
}

// ReleaseNameReply is the reply to a ReleaseName call.
type ReleaseNameReply uint32

const (
	ReleaseNameReplyReleased ReleaseNameReply = 1 + iota
	ReleaseNameReplyNonExistent
	ReleaseNameReplyNotOwner
)

func (rep ReleaseNameReply) String() string {
	switch rep {
	case ReleaseNameReplyReleased:
		return "released"
	case ReleaseNameReplyNonExistent:
		return "non existent"
	case ReleaseNameReplyNotOwner:
		return "not owner"
	}
	return "unknown"
}

// RequestNameFlags represents the possible flags for a RequestName call.
type RequestNameFlags uint32

const (
	NameFlagAllowReplacement RequestNameFlags = 1 << iota
	NameFlagReplaceExisting
	NameFlagDoNotQueue
)

// RequestNameReply is the reply to a RequestName call.
type RequestNameReply uint32

const (
	RequestNameReplyPrimaryOwner RequestNameReply = 1 + iota
	RequestNameReplyInQueue
	RequestNameReplyExists
	RequestNameReplyAlreadyOwner
)

func (rep RequestNameReply) String() string {
	switch rep {
	case RequestNameReplyPrimaryOwner:
		return "primary owner"
	case RequestNameReplyInQueue:
		return "in queue"
	case RequestNameReplyExists:
		return "exists"
	case RequestNameReplyAlreadyOwner:
		return "already owner"
	}
	return "unknown"
}
