package dbus

import (
	"context"
	"errors"
	"strings"
)

// BusObject is the interface of a remote object on which methods can be
// invoked.
type BusObject interface {
	Call(method string, flags Flags, args ...interface{}) *Call
	CallWithContext(ctx context.Context, method string, flags Flags, args ...interface{}) *Call
	Go(method string, flags Flags, ch chan *Call, args ...interface{}) *Call
	GoWithContext(ctx context.Context, method string, flags Flags, ch chan *Call, args ...interface{}) *Call
	AddMatchSignal(iface, member string, options ...MatchOption) *Call
	RemoveMatchSignal(iface, member string, options ...MatchOption) *Call
	GetProperty(p string) (Variant, error)
	SetProperty(p string, v interface{}) error
	Destination() string
	Path() ObjectPath
}

// Object represents a remote object on which methods can be invoked.
type Object struct {
	conn *Conn
	dest string
	path ObjectPath
}

// Call calls a method with (*Object).Go and waits for its reply.
func (o *Object) Call(method string, flags Flags, args ...interface{}) *Call {
	return <-o.createCall(context.Background(), method, flags, make(chan *Call, 1), args...).Done
}

// CallWithContext acts like Call but takes a context
func (o *Object) CallWithContext(ctx context.Context, method string, flags Flags, args ...interface{}) *Call {
	return <-o.createCall(ctx, method, flags, make(chan *Call, 1), args...).Done
}

// MatchOption specifies option for dbus routing match rule. Options can be constructed with WithMatch* helpers.
// For full list of available options consult
// https://dbus.freedesktop.org/doc/dbus-specification.html#message-bus-routing-match-rules
type MatchOption struct {
	key   string
	value string
}

// WithMatchOption creates match option with given key and value
func WithMatchOption(key, value string) MatchOption {
	return MatchOption{key, value}
}

// WithMatchObjectPath creates match option that filters events based on given path
func WithMatchObjectPath(path ObjectPath) MatchOption {
	return MatchOption{"path", string(path)}
}

func formatMatchOptions(options []MatchOption) string {
	items := make([]string, 0, len(options))
	for _, option := range options {
		items = append(items, option.key+"='"+option.value+"'")
	}

	return strings.Join(items, ",")
}

// AddMatchSignal subscribes BusObject to signals from specified interface,
// method (member). Additional filter rules can be added via WithMatch* option constructors.
// Note: To filter events by object path you have to specify this path via an option.
func (o *Object) AddMatchSignal(iface, member string, options ...MatchOption) *Call {
	base := []MatchOption{
		{"type", "signal"},
		{"interface", iface},
		{"member", member},
	}

	options = append(base, options...)
	return o.conn.BusObject().Call(
		"org.freedesktop.DBus.AddMatch",
		0,
		formatMatchOptions(options),
	)
}

// RemoveMatchSignal unsubscribes BusObject from signals from specified interface,
// method (member). Additional filter rules can be added via WithMatch* option constructors
func (o *Object) RemoveMatchSignal(iface, member string, options ...MatchOption) *Call {
	base := []MatchOption{
		{"type", "signal"},
		{"interface", iface},
		{"member", member},
	}

	options = append(base, options...)
	return o.conn.BusObject().Call(
		"org.freedesktop.DBus.RemoveMatch",
		0,
		formatMatchOptions(options),
	)
}

// Go calls a method with the given arguments asynchronously. It returns a
// Call structure representing this method call. The passed channel will
// return the same value once the call is done. If ch is nil, a new channel
// will be allocated. Otherwise, ch has to be buffered or Go will panic.
//
// If the flags include FlagNoReplyExpected, ch is ignored and a Call structure
// is returned with any error in Err and a closed channel in Done containing
// the returned Call as it's one entry.
//
// If the method parameter contains a dot ('.'), the part before the last dot
// specifies the interface on which the method is called.
func (o *Object) Go(method string, flags Flags, ch chan *Call, args ...interface{}) *Call {
	return o.createCall(context.Background(), method, flags, ch, args...)
}

// GoWithContext acts like Go but takes a context
func (o *Object) GoWithContext(ctx context.Context, method string, flags Flags, ch chan *Call, args ...interface{}) *Call {
	return o.createCall(ctx, method, flags, ch, args...)
}

func (o *Object) createCall(ctx context.Context, method string, flags Flags, ch chan *Call, args ...interface{}) *Call {
	if ctx == nil {
		panic("nil context")
	}
	iface := ""
	i := strings.LastIndex(method, ".")
	if i != -1 {
		iface = method[:i]
	}
	method = method[i+1:]
	msg := new(Message)
	msg.Type = TypeMethodCall
	msg.serial = o.conn.getSerial()
	msg.Flags = flags & (FlagNoAutoStart | FlagNoReplyExpected)
	msg.Headers = make(map[HeaderField]Variant)
	msg.Headers[FieldPath] = MakeVariant(o.path)
	msg.Headers[FieldDestination] = MakeVariant(o.dest)
	msg.Headers[FieldMember] = MakeVariant(method)
	if iface != "" {
		msg.Headers[FieldInterface] = MakeVariant(iface)
	}
	msg.Body = args
	if len(args) > 0 {
		msg.Headers[FieldSignature] = MakeVariant(SignatureOf(args...))
	}
	if msg.Flags&FlagNoReplyExpected == 0 {
		if ch == nil {
			ch = make(chan *Call, 1)
		} else if cap(ch) == 0 {
			panic("dbus: unbuffered channel passed to (*Object).Go")
		}
		ctx, cancel := context.WithCancel(ctx)
		call := &Call{
			Destination: o.dest,
			Path:        o.path,
			Method:      method,
			Args:        args,
			Done:        ch,
			ctxCanceler: cancel,
			ctx:         ctx,
		}
		o.conn.calls.track(msg.serial, call)
		o.conn.sendMessageAndIfClosed(msg, func() {
			o.conn.calls.handleSendError(msg, ErrClosed)
			cancel()
		})
		go func() {
			<-ctx.Done()
			o.conn.calls.handleSendError(msg, ctx.Err())
		}()

		return call
	}
	done := make(chan *Call, 1)
	call := &Call{
		Err:  nil,
		Done: done,
	}
	defer func() {
		call.Done <- call
		close(done)
	}()
	o.conn.sendMessageAndIfClosed(msg, func() {
		call.Err = ErrClosed
	})
	return call
}

// GetProperty calls org.freedesktop.DBus.Properties.Get on the given
// object. The property name must be given in interface.member notation.
func (o *Object) GetProperty(p string) (Variant, error) {
	idx := strings.LastIndex(p, ".")
	if idx == -1 || idx+1 == len(p) {
		return Variant{}, errors.New("dbus: invalid property " + p)
	}

	iface := p[:idx]
	prop := p[idx+1:]

	result := Variant{}
	err := o.Call("org.freedesktop.DBus.Properties.Get", 0, iface, prop).Store(&result)

	if err != nil {
		return Variant{}, err
	}

	return result, nil
}

// SetProperty calls org.freedesktop.DBus.Properties.Set on the given
// object. The property name must be given in interface.member notation.
func (o *Object) SetProperty(p string, v interface{}) error {
	idx := strings.LastIndex(p, ".")
	if idx == -1 || idx+1 == len(p) {
		return errors.New("dbus: invalid property " + p)
	}

	iface := p[:idx]
	prop := p[idx+1:]

	return o.Call("org.freedesktop.DBus.Properties.Set", 0, iface, prop, v).Err
}

// Destination returns the destination that calls on (o *Object) are sent to.
func (o *Object) Destination() string {
	return o.dest
}

// Path returns the path that calls on (o *Object") are sent to.
func (o *Object) Path() ObjectPath {
	return o.path
}
