package darwin

import (
	"context"
	"fmt"
	"log"
	"sync"

	"github.com/JuulLabs-OSS/ble"
	"github.com/JuulLabs-OSS/cbgo"
)

// newGenConn creates a new generic (role-less) connection.  This should not be
// called directly; use newCentralConn or newPeripheralConn instead.
func newGenConn(d *Device, a ble.Addr) (*conn, error) {
	c := &conn{
		dev:   d,
		rxMTU: 23,
		txMTU: 23,
		addr:  a,
		done:  make(chan struct{}),

		notifiers: make(map[cbgo.Characteristic]ble.Notifier),

		subs:     make(map[string]*sub),
		chrReads: make(map[string]chan error),
	}

	err := d.addConn(c)
	if err != nil {
		return nil, err
	}

	go func() {
		<-c.done
		d.delConn(c.addr)
	}()

	return c, nil
}

// newCentralConn creates a new connection with us acting as central
// (peer=peripheral).
func newCentralConn(d *Device, prph cbgo.Peripheral) (*conn, error) {
	c, err := newGenConn(d, ble.NewAddr(prph.Identifier().String()))
	if err != nil {
		return nil, err
	}

	// -3 to account for WriteCommand base.
	c.txMTU = prph.MaximumWriteValueLength(false) - 3
	c.prph = prph

	return c, nil
}

// newCentralConn creates a new connection with us acting as peripheral
// (peer=central).
func newPeripheralConn(d *Device, cent cbgo.Central) (*conn, error) {
	c, err := newGenConn(d, ble.NewAddr(cent.Identifier().String()))
	if err != nil {
		return nil, err
	}

	// -3 to account for ATT_HANDLE_VALUE_NTF base.
	c.txMTU = cent.MaximumUpdateValueLength() - 3
	c.cent = cent

	return c, nil
}

type conn struct {
	sync.RWMutex

	dev   *Device
	ctx   context.Context
	rxMTU int
	txMTU int
	addr  ble.Addr
	done  chan struct{}

	evl clientEventListener

	prph cbgo.Peripheral
	cent cbgo.Central

	notifiers map[cbgo.Characteristic]ble.Notifier // central connection only

	subs     map[string]*sub
	chrReads map[string](chan error)
}

func (c *conn) Context() context.Context {
	return c.ctx
}

func (c *conn) SetContext(ctx context.Context) {
	c.ctx = ctx
}

func (c *conn) LocalAddr() ble.Addr {
	// return c.dev.Address()
	return c.addr // FIXME
}

func (c *conn) RemoteAddr() ble.Addr {
	return c.addr
}

func (c *conn) RxMTU() int {
	return c.rxMTU
}

func (c *conn) SetRxMTU(mtu int) {
	c.rxMTU = mtu
}

func (c *conn) TxMTU() int {
	return c.txMTU
}

func (c *conn) SetTxMTU(mtu int) {
	c.Lock()
	c.txMTU = mtu
	c.Unlock()
}

func (c *conn) Read(b []byte) (int, error) {
	return 0, nil
}
func (c *conn) Write(b []byte) (int, error) {
	return 0, nil
}

func (c *conn) Close() error {
	c.evl.Close()
	return nil
}

// Disconnected returns a receiving channel, which is closed when the connection disconnects.
func (c *conn) Disconnected() <-chan struct{} {
	return c.done
}

// processChrRead handles an incoming read response.  CoreBluetooth does not
// distinguish explicit reads from unsolicited notifications.  This function
// identifies which type the incoming message is.
func (c *conn) processChrRead(err error, cbchr cbgo.Characteristic) {
	c.RLock()
	defer c.RUnlock()

	uuidStr := uuidStrWithDashes(cbchr.UUID().String())
	found := false

	ch := c.chrReads[uuidStr]
	if ch != nil {
		ch <- err
		found = true
	}

	s := c.subs[uuidStr]
	if s != nil {
		s.fn(cbchr.Value())
		found = true
	}

	if !found {
		log.Printf("received characteristic read response without corresponding request: uuid=%s", uuidStr)
	}
}

// addChrReader starts listening for a solicited read response.
func (c *conn) addChrReader(char *ble.Characteristic) (chan error, error) {
	uuidStr := uuidStrWithDashes(char.UUID.String())

	c.Lock()
	defer c.Unlock()

	if c.chrReads[uuidStr] != nil {
		return nil, fmt.Errorf("cannot read from the same attribute twice: uuid=%s", uuidStr)
	}

	ch := make(chan error)
	c.chrReads[uuidStr] = ch

	return ch, nil
}

// delChrReader stops listening for a solicited read response.
func (c *conn) delChrReader(char *ble.Characteristic) {
	c.Lock()
	defer c.Unlock()

	uuidStr := uuidStrWithDashes(char.UUID.String())
	delete(c.chrReads, uuidStr)
}

// addSub starts listening for unsolicited notifications and indications for a
// particular characteristic.
func (c *conn) addSub(char *ble.Characteristic, fn ble.NotificationHandler) {
	uuidStr := uuidStrWithDashes(char.UUID.String())

	c.Lock()
	defer c.Unlock()

	// It feels like we should return an error if we are already subscribed to
	// this characteristic.  Just quietly overwrite the existing handler to
	// preserve backwards compatibility.

	c.subs[uuidStr] = &sub{
		fn:   fn,
		char: char,
	}
}

// delSub stops listening for unsolicited notifications and indications for a
// particular characteristic.
func (c *conn) delSub(char *ble.Characteristic) {
	uuidStr := uuidStrWithDashes(char.UUID.String())

	c.Lock()
	defer c.Unlock()

	delete(c.subs, uuidStr)
}
