package ble

import (
	"context"
	"os"
	"os/signal"
	"syscall"

	"github.com/pkg/errors"
)

// ErrDefaultDevice ...
var ErrDefaultDevice = errors.New("default device is not set")

var defaultDevice Device

// SetDefaultDevice returns the default HCI device.
func SetDefaultDevice(d Device) {
	defaultDevice = d
}

// AddService adds a service to database.
func AddService(svc *Service) error {
	if defaultDevice == nil {
		return ErrDefaultDevice
	}
	return defaultDevice.AddService(svc)
}

// RemoveAllServices removes all services that are currently in the database.
func RemoveAllServices() error {
	if defaultDevice == nil {
		return ErrDefaultDevice
	}
	return defaultDevice.RemoveAllServices()
}

// SetServices set the specified service to the database.
// It removes all currently added services, if any.
func SetServices(svcs []*Service) error {
	if defaultDevice == nil {
		return ErrDefaultDevice
	}
	return defaultDevice.SetServices(svcs)
}

// Stop detatch the GATT server from a peripheral device.
func Stop() error {
	if defaultDevice == nil {
		return ErrDefaultDevice
	}
	return defaultDevice.Stop()
}

// AdvertiseNameAndServices advertises device name, and specified service UUIDs.
// It tres to fit the UUIDs in the advertising packet as much as possi
// If name doesn't fit in the advertising packet, it will be put in scan response.
func AdvertiseNameAndServices(ctx context.Context, name string, uuids ...UUID) error {
	if defaultDevice == nil {
		return ErrDefaultDevice
	}
	defer untrap(trap(ctx))
	return defaultDevice.AdvertiseNameAndServices(ctx, name, uuids...)
}

// AdvertiseIBeaconData advertise iBeacon with given manufacturer data.
func AdvertiseIBeaconData(ctx context.Context, b []byte) error {
	if defaultDevice == nil {
		return ErrDefaultDevice
	}
	defer untrap(trap(ctx))
	return defaultDevice.AdvertiseIBeaconData(ctx, b)
}

// AdvertiseIBeacon advertises iBeacon with specified parameters.
func AdvertiseIBeacon(ctx context.Context, u UUID, major, minor uint16, pwr int8) error {
	if defaultDevice == nil {
		return ErrDefaultDevice
	}
	defer untrap(trap(ctx))
	return defaultDevice.AdvertiseIBeacon(ctx, u, major, minor, pwr)
}

// Scan starts scanning. Duplicated advertisements will be filtered out if allowDup is set to false.
func Scan(ctx context.Context, allowDup bool, h AdvHandler, f AdvFilter) error {
	if defaultDevice == nil {
		return ErrDefaultDevice
	}
	defer untrap(trap(ctx))

	if f == nil {
		return defaultDevice.Scan(ctx, allowDup, h)
	}

	h2 := func(a Advertisement) {
		if f(a) {
			h(a)
		}
	}
	return defaultDevice.Scan(ctx, allowDup, h2)
}

// Find ...
func Find(ctx context.Context, allowDup bool, f AdvFilter) ([]Advertisement, error) {
	if defaultDevice == nil {
		return nil, ErrDefaultDevice
	}
	var advs []Advertisement
	h := func(a Advertisement) {
		advs = append(advs, a)
	}
	defer untrap(trap(ctx))
	return advs, Scan(ctx, allowDup, h, f)
}

// Dial ...
func Dial(ctx context.Context, a Addr) (Client, error) {
	if defaultDevice == nil {
		return nil, ErrDefaultDevice
	}
	defer untrap(trap(ctx))
	return defaultDevice.Dial(ctx, a)
}

// Connect searches for and connects to a Peripheral which matches specified condition.
func Connect(ctx context.Context, f AdvFilter) (Client, error) {
	ctx2, cancel := context.WithCancel(ctx)
	go func() {
		select {
		case <-ctx.Done():
			cancel()
		case <-ctx2.Done():
		}
	}()

	ch := make(chan Advertisement)
	fn := func(a Advertisement) {
		cancel()
		ch <- a
	}
	if err := Scan(ctx2, false, fn, f); err != nil {
		if err != context.Canceled {
			return nil, errors.Wrap(err, "can't scan")
		}
	}

	cln, err := Dial(ctx, (<-ch).Addr())
	return cln, errors.Wrap(err, "can't dial")
}

// A NotificationHandler handles notification or indication from a server.
type NotificationHandler func(req []byte)

// WithSigHandler ...
func WithSigHandler(ctx context.Context, cancel func()) context.Context {
	return context.WithValue(ctx, ContextKeySig, cancel)
}

// Cleanup for the interrupted case.
func trap(ctx context.Context) chan<- os.Signal {
	v := ctx.Value(ContextKeySig)
	if v == nil {
		return nil
	}
	cancel, ok := v.(func())
	if cancel == nil || !ok {
		return nil
	}

	sigs := make(chan os.Signal, 1)
	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
	go func() {
		select {
		case <-sigs:
			cancel()
		case <-ctx.Done():
		}
	}()
	return sigs
}

func untrap(sigs chan<- os.Signal) {
	if sigs == nil {
		return
	}
	signal.Stop(sigs)
}
