package infinitime

import (
	"bytes"
	"context"
	"encoding/binary"
	"errors"
	"reflect"
	"strings"
	"time"

	"github.com/fxamacker/cbor/v2"
	bt "github.com/muka/go-bluetooth/api"
	"github.com/muka/go-bluetooth/bluez"
	"github.com/muka/go-bluetooth/bluez/profile/adapter"
	"github.com/muka/go-bluetooth/bluez/profile/device"
	"github.com/muka/go-bluetooth/bluez/profile/gatt"
	"github.com/rs/zerolog"
	"go.arsenm.dev/infinitime/blefs"
)

// This global is used to store the logger.
// log.Logger is not used as it would interfere
// with the package importing the library
var log zerolog.Logger

const BTName = "InfiniTime"

const (
	NewAlertChar    = "00002a46-0000-1000-8000-00805f9b34fb"
	NotifEventChar  = "00020001-78fc-48fe-8e23-433b3a1942d0"
	StepCountChar   = "00030001-78fc-48fe-8e23-433b3a1942d0"
	MotionValChar   = "00030002-78fc-48fe-8e23-433b3a1942d0"
	FirmwareVerChar = "00002a26-0000-1000-8000-00805f9b34fb"
	CurrentTimeChar = "00002a2b-0000-1000-8000-00805f9b34fb"
	BatteryLvlChar  = "00002a19-0000-1000-8000-00805f9b34fb"
	HeartRateChar   = "00002a37-0000-1000-8000-00805f9b34fb"
	FSTransferChar  = "adaf0200-4669-6c65-5472-616e73666572"
	FSVersionChar   = "adaf0100-4669-6c65-5472-616e73666572"
	WeatherDataChar = "00040001-78fc-48fe-8e23-433b3a1942d0"
	NavFlagsChar    = "00010001-78fc-48fe-8e23-433b3a1942d0"
	NavNarrativeChar= "00010002-78fc-48fe-8e23-433b3a1942d0"
	NavManDistChar  = "00010003-78fc-48fe-8e23-433b3a1942d0"
	NavProgressChar = "00010004-78fc-48fe-8e23-433b3a1942d0"
)

var charNames = map[string]string{
	NewAlertChar:    "New Alert",
	NotifEventChar:  "Notification Event",
	StepCountChar:   "Step Count",
	MotionValChar:   "Motion Values",
	FirmwareVerChar: "Firmware Version",
	CurrentTimeChar: "Current Time",
	BatteryLvlChar:  "Battery Level",
	HeartRateChar:   "Heart Rate",
	FSTransferChar:  "Filesystem Transfer",
	FSVersionChar:   "Filesystem Version",
	WeatherDataChar: "Weather Data",
	NavFlagsChar:    "Navigation Icon",
	NavNarrativeChar:"Navigation Instruction",
	NavManDistChar:  "Navigation Distance to next event",
	NavProgressChar: "Navigation Progress",
}

var NavFlagNames = []string{
	"arrive", "arrive-left", "arrive-right", "arrive-straight",
	"close",
	"continue", "continue-left", "continue-right", "continue-slight-left",
	"continue-slight-right", "continue-straight", "continue-uturn",
	"depart", "depart-left", "depart-right", "depart-straight",
	"end-of-road-left", "end-of-road-right",
	"ferry", "flag",
	"fork", "fork-left", "fork-right", "fork-straight",
	"fork-slight-left", "fork-slight-right",
	"invalid", "invalid-left", "invalid-right",
	"invalid-slight-left", "invalid-slight-right",
	"invalid-straight", "invalid-uturn",
	"merge-left", "merge-right", "merge-slight-left",
	"merge-slight-right", "merge-straight",
	"new-name-left", "new-name-right", "new-name-sharp-left", "new-name-sharp-right",
	"new-name-slight-left", "new-name-slight-right", "new-name-straight",
	"notification-left", "notification-right",
	"notification-sharp-left", "notification-sharp-right",
	"notification-slight-left", "notification-slight-right",
	"notification-straight",
	"off-ramp-left", "off-ramp-right", "off-ramp-sharp-left", "off-ramp-sharp-right",
	"off-ramp-slight-left", "off-ramp-slight-right", "off-ramp-straight",
	"on-ramp-left", "on-ramp-right", "on-ramp-sharp-left", "on-ramp-sharp-right",
	"on-ramp-slight-left", "on-ramp-slight-right", "on-ramp-straight",
	"rotary", "rotary-left", "rotary-right", "rotary-sharp-left", "rotary-sharp-right",
	"rotary-slight-left", "rotary-slight-right", "rotary-straight",
	"roundabout", "roundabout-left", "roundabout-right",
	"roundabout-sharp-left", "roundabout-sharp-right",
	"roundabout-slight-left", "roundabout-slight-right", "roundabout-straight",
	"turn-left", "turn-right", "turn-sharp-left", "turn-sharp-right",
	"turn-slight-left", "turn-slight-right", "turn-stright",
	"updown", "uturn",
}

type NavigationEvent struct {
	flag         string
	narrative    string
	dist         string
	progress     uint8
}

type Device struct {
	device          *device.Device1
	navflagsChar    *gatt.GattCharacteristic1
	navnarrativeChar*gatt.GattCharacteristic1
	navmandistChar  *gatt.GattCharacteristic1
	navprogressChar *gatt.GattCharacteristic1
	newAlertChar    *gatt.GattCharacteristic1
	notifEventChar  *gatt.GattCharacteristic1
	stepCountChar   *gatt.GattCharacteristic1
	motionValChar   *gatt.GattCharacteristic1
	fwVersionChar   *gatt.GattCharacteristic1
	currentTimeChar *gatt.GattCharacteristic1
	battLevelChar   *gatt.GattCharacteristic1
	heartRateChar   *gatt.GattCharacteristic1
	fsVersionChar   *gatt.GattCharacteristic1
	fsTransferChar  *gatt.GattCharacteristic1
	weatherDataChar *gatt.GattCharacteristic1
	notifEventCh    chan uint8
	notifEventDone  bool
	Music           MusicCtrl
	DFU             DFU
	navigationEv    NavigationEvent
}

var (
	ErrNoDevices        = errors.New("no InfiniTime devices found")
	ErrNotFound         = errors.New("could not find any advertising InfiniTime devices")
	ErrNotConnected     = errors.New("not connected")
	ErrNoTimelineHeader = errors.New("events must contain the timeline header")
	ErrPairTimeout      = errors.New("reached timeout while pairing")
	ErrNavProgress      = errors.New("progress needs to between 0 and 100")
	ErrNavInvalidFlag   = errors.New("this flag is invalid")
)

type ErrCharNotAvail struct {
	uuid string
}

func (e ErrCharNotAvail) Error() string {
	return "characteristic " + e.uuid + " (" + charNames[e.uuid] + ") not available"
}

type Options struct {
	AttemptReconnect bool
	WhitelistEnabled bool
	Whitelist        []string
	OnReqPasskey     func() (uint32, error)
	OnReconnect      func()
	Logger           zerolog.Logger
	LogLevel         zerolog.Level
}

var DefaultOptions = &Options{
	AttemptReconnect: true,
	WhitelistEnabled: false,
	Logger:           zerolog.Nop(),
	LogLevel:         zerolog.Disabled,
}

// Connect will attempt to connect to a
// paired InfiniTime device. If none are paired,
// it will attempt to discover and pair one.
//
// It will also attempt to reconnect to the device
// if it disconnects and that is enabled in the options.
func Connect(ctx context.Context, opts *Options) (*Device, error) {
	if opts == nil {
		opts = DefaultOptions
	}

	log = opts.Logger.Level(opts.LogLevel)

	// Set passkey request callback
	setOnPasskeyReq(opts.OnReqPasskey)

	// Connect to bluetooth device
	btDev, err := connect(ctx, opts, true)
	if err != nil {
		return nil, err
	}

	// Create new device
	out := &Device{device: btDev}

	// Resolve characteristics
	err = out.resolveChars()
	if err != nil {
		return nil, err
	}

	return out, nil
}

// connect connects to the InfiniTime bluez device
func connect(ctx context.Context, opts *Options, first bool) (dev *device.Device1, err error) {
	// Get devices
	devs, err := defaultAdapter.GetDevices()
	if err != nil {
		return nil, err
	}

	// For every device
	for _, listDev := range devs {
		// If device name does not match, skip
		if listDev.Properties.Name != BTName {
			continue
		}
		// If whitelist enabled and doesn't contain
		// device, skip
		if opts.WhitelistEnabled &&
			!contains(opts.Whitelist, listDev.Properties.Address) {
			log.Debug().
				Str("mac", listDev.Properties.Address).
				Msg("InfiniTime device skipped as it is not in whitelist")
			continue
		}

		// Set device
		dev = listDev

		log.Debug().
			Str("mac", dev.Properties.Address).
			Msg("InfiniTime device found in list")

		break
	}

	// If device not set
	if dev == nil {
		log.Debug().Msg("No device found in list, attempting to discover")
		// Discover devices on adapter
		discoverCh, cancel, err := bt.Discover(defaultAdapter, &adapter.DiscoveryFilter{Transport: "le"})
		if err != nil {
			return nil, err
		}

	discoverLoop:
		for {
			select {
			case event := <-discoverCh:
				// If event type is not device added, skip
				if event.Type != adapter.DeviceAdded {
					continue
				}

				// Create new device from event path
				discovered, err := device.NewDevice1(event.Path)
				if err != nil {
					return nil, err
				}

				// If device name does not match, skip
				if discovered.Properties.Name != BTName {
					continue
				}
				// If whitelist enabled and doesn't contain
				// device, skip
				if opts.WhitelistEnabled &&
					!contains(opts.Whitelist, discovered.Properties.Address) {
					log.Debug().
						Str("mac", discovered.Properties.Address).
						Msg("Discovered InfiniTime device skipped as it is not in whitelist")
					continue
				}

				// Set device
				dev = discovered

				log.Debug().
					Str("mac", dev.Properties.Address).
					Msg("InfiniTime device discovered")
				break discoverLoop
			case <-ctx.Done():
				break discoverLoop

			}
		}

		// Cancel discovery
		cancel()
	}

	// If device is still not set, return error
	if dev == nil {
		return nil, ErrNoDevices
	}

	// Create variable to track if reconnect
	// was required
	reconnRequired := false
	// If device is not connected
	if !dev.Properties.Connected {
		log.Debug().Msg("Device not connected, connecting")
		// Connect to device
		err = dev.Connect()
		if err != nil {
			return nil, err
		}
		// Set reconnect required to true
		reconnRequired = true
	}

	// If device is not paired
	if !dev.Properties.Paired {
		log.Debug().Msg("Device not paired, pairing")
		// Pair device
		err = dev.Pair()
		if err != nil {
			return nil, err
		}
	}

	// If this is the first connection and reconnect
	// is enabled, start reconnect goroutine
	if first && opts.AttemptReconnect {
		go reconnect(ctx, opts, dev)
	}

	// If this is not the first connection, a reonnect
	// was required, and the OnReconnect callback exists,
	// run it
	if !first && reconnRequired && opts.OnReconnect != nil {
		log.Debug().Msg("Reconnected to device, running OnReconnect callback")
		opts.OnReconnect()
	}

	return dev, nil
}

// reconnect reconnects to a device if it disconnects
func reconnect(ctx context.Context, opts *Options, dev *device.Device1) {
	// Watch device properties
	propCh := watchProps(dev)

	// Create variables to store time of last disconnect
	// and amount of diconnects
	lastDisconnect := time.Unix(0, 0)
	amtDisconnects := 0

	for event := range propCh {
		// If event name is not Connected and value is not false, skip
		if event.Name != "Connected" && event.Value != false {
			continue
		}

		// Store seconds since last disconnect
		secsSince := time.Since(lastDisconnect).Seconds()
		// If over 3 seconds have passed, reset disconnect count
		if secsSince > 3 {
			amtDisconnects = 0
		}

		// If less than 3 seconds have passed and more than 6
		// disconnects have occurred, remove the device and reset
		if secsSince <= 3 && amtDisconnects >= 6 {
			opts.Logger.Warn().Msg("At least 6 disconnects have occurred in the last three seconds. If this continues, try removing the InfiniTime device from bluetooth.")
			lastDisconnect = time.Unix(0, 0)
			amtDisconnects = 0
		}

		// Set disconnect variables
		lastDisconnect = time.Now()
		amtDisconnects++

		for i := 0; i < 6; i++ {
			// If three tries failed, remove device
			if i == 3 {
				opts.Logger.Warn().Msg("Multiple connection attempts have failed. If this continues, try removing the InfiniTime device from bluetooth.")
			}
			// Connect to device
			newDev, err := connect(ctx, opts, false)
			if err != nil {
				time.Sleep(time.Second)
				continue
			}
			// Replace device with new device
			*dev = *newDev

			break
		}
	}
}

// bufferChannel writes all events on propCh to a new, buffered channel
func bufferChannel(propCh chan *bluez.PropertyChanged) <-chan *bluez.PropertyChanged {
	out := make(chan *bluez.PropertyChanged, 10)
	go func() {
		for event := range propCh {
			out <- event
		}
	}()
	return out
}

// watchProps returns a buffered channel for the device properties
func watchProps(dev *device.Device1) <-chan *bluez.PropertyChanged {
	uPropCh, err := dev.WatchProperties()
	if err != nil {
		panic(err)
	}
	return bufferChannel(uPropCh)
}

// setOnPasskeyReq sets the callback for a passkey request.
// It ensures the function will never be nil.
func setOnPasskeyReq(onReqPasskey func() (uint32, error)) {
	itdAgent.ReqPasskey = onReqPasskey
	if itdAgent.ReqPasskey == nil {
		itdAgent.ReqPasskey = func() (uint32, error) {
			return 0, nil
		}
	}
}

// contains checks if s is contained within ss
func contains(ss []string, s string) bool {
	for _, str := range ss {
		if strings.EqualFold(str, s) {
			return true
		}
	}
	return false
}

// resolveChars attempts to set all required
// characteristics in an InfiniTime struct
func (i *Device) resolveChars() error {
	// Get device characteristics
	chars, err := i.device.GetCharacteristics()
	if err != nil {
		return err
	}
	// While no characteristics found
	for len(chars) == 0 {
		// Sleep one second
		time.Sleep(time.Second)
		// Attempt to retry getting characteristics
		chars, err = i.device.GetCharacteristics()
		if err != nil {
			return err
		}
	}
	// For every discovered characteristics
	for _, char := range chars {
		charResolved := true
		// Set correct characteristics
		switch char.Properties.UUID {
		case NavFlagsChar:
			i.navflagsChar = char
		case NavNarrativeChar:
			i.navnarrativeChar = char
		case NavManDistChar:
			i.navmandistChar = char
		case NavProgressChar:
			i.navprogressChar = char
		case NewAlertChar:
			i.newAlertChar = char
		case NotifEventChar:
			i.notifEventChar = char
		case StepCountChar:
			i.stepCountChar = char
		case MotionValChar:
			i.motionValChar = char
		case FirmwareVerChar:
			i.fwVersionChar = char
		case CurrentTimeChar:
			i.currentTimeChar = char
		case BatteryLvlChar:
			i.battLevelChar = char
		case HeartRateChar:
			i.heartRateChar = char
		case MusicEventChar:
			i.Music.eventChar = char
		case MusicStatusChar:
			i.Music.statusChar = char
		case MusicArtistChar:
			i.Music.artistChar = char
		case MusicTrackChar:
			i.Music.trackChar = char
		case MusicAlbumChar:
			i.Music.albumChar = char
		case DFUCtrlPointChar:
			i.DFU.ctrlPointChar = char
		case DFUPacketChar:
			i.DFU.packetChar = char
		case FSTransferChar:
			i.fsTransferChar = char
		case FSVersionChar:
			i.fsVersionChar = char
		case WeatherDataChar:
			i.weatherDataChar = char
		default:
			charResolved = false
		}
		if charResolved {
			log.Debug().
				Str("uuid", char.Properties.UUID).
				Str("name", charNames[char.Properties.UUID]).
				Msg("Resolved characteristic")
		}
	}
	return nil
}

// Address returns the InfiniTime's bluetooth address
func (i *Device) Address() string {
	return i.device.Properties.Address
}

// Version returns InfiniTime's reported firmware version string
func (i *Device) Version() (string, error) {
	if err := i.checkStatus(i.fwVersionChar, FirmwareVerChar); err != nil {
		return "", err
	}
	ver, err := i.fwVersionChar.ReadValue(nil)
	return string(ver), err
}

// BatteryLevel gets the watch's battery level via the Battery Service
func (i *Device) BatteryLevel() (uint8, error) {
	if err := i.checkStatus(i.battLevelChar, BatteryLvlChar); err != nil {
		return 0, err
	}
	battLevel, err := i.battLevelChar.ReadValue(nil)
	if err != nil {
		return 0, err
	}
	return uint8(battLevel[0]), nil
}

func (i *Device) StepCount() (uint32, error) {
	if err := i.checkStatus(i.stepCountChar, StepCountChar); err != nil {
		return 0, err
	}
	stepCountData, err := i.stepCountChar.ReadValue(nil)
	if err != nil {
		return 0, err
	}
	return binary.LittleEndian.Uint32(stepCountData), nil
}

type MotionValues struct {
	X int16
	Y int16
	Z int16
}

func (i *Device) Motion() (MotionValues, error) {
	out := MotionValues{}
	if err := i.checkStatus(i.motionValChar, MotionValChar); err != nil {
		return out, err
	}
	motionVals, err := i.motionValChar.ReadValue(nil)
	if err != nil {
		return out, nil
	}
	motionValReader := bytes.NewReader(motionVals)
	err = binary.Read(motionValReader, binary.LittleEndian, &out)
	if err != nil {
		return out, err
	}
	return out, nil
}

func (i *Device) HeartRate() (uint8, error) {
	if err := i.checkStatus(i.heartRateChar, HeartRateChar); err != nil {
		return 0, err
	}
	heartRate, err := i.heartRateChar.ReadValue(nil)
	if err != nil {
		return 0, err
	}
	return uint8(heartRate[1]), nil
}

func (i *Device) WatchHeartRate(ctx context.Context) (<-chan uint8, error) {
	if err := i.checkStatus(i.heartRateChar, HeartRateChar); err != nil {
		return nil, err
	}
	// Start notifications on heart rate characteristic
	err := i.heartRateChar.StartNotify()
	if err != nil {
		return nil, err
	}
	// Watch characteristics of heart rate characteristic
	ch, err := i.heartRateChar.WatchProperties()
	if err != nil {
		return nil, err
	}
	out := make(chan uint8, 2)
	currentHeartRate, err := i.HeartRate()
	if err != nil {
		return nil, err
	}
	out <- currentHeartRate
	go func() {
		// For every event
		for {
			select {
			case <-ctx.Done():
				log.Debug().Str("func", "WatchMotion").Msg("Received done signal")
				close(out)
				i.heartRateChar.StopNotify()
				return
			case event := <-ch:
				// If value changed
				if event.Name == "Value" {
					// Send heart rate to channel
					out <- uint8(event.Value.([]byte)[1])
				} else if event.Name == "Notifying" && !event.Value.(bool) {
					log.Debug().Str("func", "WatchMotion").Msg("Notifications stopped, restarting")
					i.heartRateChar.StartNotify()
				}
			}
		}
	}()
	return out, nil
}

func (i *Device) WatchBatteryLevel(ctx context.Context) (<-chan uint8, error) {
	if err := i.checkStatus(i.battLevelChar, BatteryLvlChar); err != nil {
		return nil, err
	}
	// Start notifications on heart rate characteristic
	err := i.battLevelChar.StartNotify()
	if err != nil {
		return nil, err
	}
	// Watch characteristics of heart rate characteristic
	ch, err := i.battLevelChar.WatchProperties()
	if err != nil {
		return nil, err
	}
	out := make(chan uint8, 2)
	currentBattLevel, err := i.BatteryLevel()
	if err != nil {
		return nil, err
	}
	out <- currentBattLevel
	go func() {
		// For every event
		for {
			select {
			case <-ctx.Done():
				log.Debug().Str("func", "WatchMotion").Msg("Received done signal")
				close(out)
				i.battLevelChar.StopNotify()
				return
			case event := <-ch:
				// If value changed
				if event.Name == "Value" {
					// Send heart rate to channel
					out <- uint8(event.Value.([]byte)[0])
				} else if event.Name == "Notifying" && !event.Value.(bool) {
					log.Debug().Str("func", "WatchMotion").Msg("Notifications stopped, restarting")
					i.battLevelChar.StartNotify()
				}
			}
		}
	}()
	return out, nil
}

func (i *Device) WatchStepCount(ctx context.Context) (<-chan uint32, error) {
	if err := i.checkStatus(i.stepCountChar, StepCountChar); err != nil {
		return nil, err
	}
	// Start notifications on step count characteristic
	err := i.stepCountChar.StartNotify()
	if err != nil {
		return nil, err
	}
	// Watch properties of step count characteristic
	ch, err := i.stepCountChar.WatchProperties()
	if err != nil {
		return nil, err
	}
	out := make(chan uint32, 2)
	currentStepCount, err := i.StepCount()
	if err != nil {
		return nil, err
	}
	out <- currentStepCount
	go func() {
		// For every event
		for {
			select {
			case <-ctx.Done():
				log.Debug().Str("func", "WatchMotion").Msg("Received done signal")
				close(out)
				i.stepCountChar.StopNotify()
				return
			case event := <-ch:
				// If value changed
				if event.Name == "Value" {
					// Send step count to channel
					out <- binary.LittleEndian.Uint32(event.Value.([]byte))
				} else if event.Name == "Notifying" && !event.Value.(bool) {
					log.Debug().Str("func", "WatchMotion").Msg("Notifications stopped, restarting")
					i.stepCountChar.StartNotify()
				}
			}
		}
	}()
	return out, nil
}

func (i *Device) WatchMotion(ctx context.Context) (<-chan MotionValues, error) {
	if err := i.checkStatus(i.motionValChar, MotionValChar); err != nil {
		return nil, err
	}
	// Start notifications on motion characteristic
	err := i.motionValChar.StartNotify()
	if err != nil {
		return nil, err
	}
	// Watch properties of motion characteristic
	ch, err := i.motionValChar.WatchProperties()
	if err != nil {
		return nil, err
	}
	out := make(chan MotionValues, 2)
	motionVals, err := i.Motion()
	if err != nil {
		return nil, err
	}
	out <- motionVals
	go func() {
		// For every event
		for {
			select {
			case <-ctx.Done():
				log.Debug().Str("func", "WatchMotion").Msg("Received done signal")
				close(out)
				i.motionValChar.StopNotify()
				return
			case event := <-ch:
				// If value changed
				if event.Name == "Value" {
					// Read binary into MotionValues struct
					binary.Read(bytes.NewReader(event.Value.([]byte)), binary.LittleEndian, &motionVals)
					// Send step count to channel
					out <- motionVals
				} else if event.Name == "Notifying" && !event.Value.(bool) {
					log.Debug().Str("func", "WatchMotion").Msg("Notifications stopped, restarting")
					i.motionValChar.StartNotify()
				}
			}

		}
	}()
	return out, nil
}

// SetTime sets the watch's time using the Current Time Service
func (i *Device) SetTime(t time.Time) error {
	if err := i.checkStatus(i.currentTimeChar, CurrentTimeChar); err != nil {
		return err
	}
	buf := &bytes.Buffer{}
	binary.Write(buf, binary.LittleEndian, uint16(t.Year()))
	binary.Write(buf, binary.LittleEndian, uint8(t.Month()))
	binary.Write(buf, binary.LittleEndian, uint8(t.Day()))
	binary.Write(buf, binary.LittleEndian, uint8(t.Hour()))
	binary.Write(buf, binary.LittleEndian, uint8(t.Minute()))
	binary.Write(buf, binary.LittleEndian, uint8(t.Second()))
	binary.Write(buf, binary.LittleEndian, uint8(t.Weekday()))
	binary.Write(buf, binary.LittleEndian, uint8((t.Nanosecond()/1000)/1e6*256))
	binary.Write(buf, binary.LittleEndian, uint8(0b0001))
	return i.currentTimeChar.WriteValue(buf.Bytes(), nil)
}

// Notify sends a notification to InfiniTime via
// the Alert Notification Service (ANS)
func (i *Device) Notify(title, body string) error {
	if err := i.checkStatus(i.newAlertChar, NewAlertChar); err != nil {
		return err
	}
	return i.newAlertChar.WriteValue(
		append([]byte{0x00, 0x01, 0x00}, []byte(title+"\x00"+body)...),
		nil,
	)
}

func CheckValidNavFlag(flag string) error {
	for _, v := range NavFlagNames {
		if v == flag {
			return nil
		}
	}
	return ErrNavInvalidFlag
}

// Navigation sends a NavigationEvent to the watch
func (i *Device) Navigation(flag string, narrative string, dist string, progress uint8) error {
	if progress > 100 {
		return ErrNavProgress
	}
	if err := CheckValidNavFlag(flag); err != nil {
		return err
	}

	if flag != i.navigationEv.flag {
		log.Debug().Str("func", "Navigation").
		    Msg("Sending flag")
		if err := i.checkStatus(i.navflagsChar, NavFlagsChar); err != nil {
			return err
		}
		if err := i.navflagsChar.WriteValue([]byte(flag), nil); err != nil {
			return err
		}
		i.navigationEv.flag = flag
	}

	if narrative != i.navigationEv.narrative {
		log.Debug().Str("func", "Navigation").
		    Msg("Sending narrative")
		if err := i.checkStatus(i.navnarrativeChar, NavNarrativeChar); err != nil {
			return err
		}
		if err := i.navnarrativeChar.WriteValue([]byte(narrative), nil); err != nil {
			return err
		}
		i.navigationEv.narrative = narrative
	}

	if dist != i.navigationEv.dist {
		log.Debug().Str("func", "Navigation").
		    Msg("Sending mandist")
		if err := i.checkStatus(i.navmandistChar, NavManDistChar); err != nil {
			return err
		}
		if err := i.navmandistChar.WriteValue([]byte(dist), nil); err != nil {
			return err
		}
		i.navigationEv.dist = dist
	}

	if progress != i.navigationEv.progress {
		log.Debug().Str("func", "Navigation").
		    Msg("Sending progress")
		if err := i.checkStatus(i.navprogressChar, NavProgressChar); err != nil {
			return err
		}
		buf := &bytes.Buffer{}
		binary.Write(buf, binary.LittleEndian, progress)
		if err := i.navprogressChar.WriteValue(buf.Bytes(), nil); err != nil {
			return err
		}
		i.navigationEv.progress = progress
	}

	return nil
}

// These constants represent the possible call statuses selected by the user
const (
	CallStatusDeclined uint8 = iota
	CallStatusAccepted
	CallStatusMuted
)

// NotifyCall sends a call notification to the PineTime and returns a channel.
// This channel will contain the user's response to the call notification.
func (i *Device) NotifyCall(from string) (<-chan uint8, error) {
	if err := i.checkStatus(i.newAlertChar, NewAlertChar); err != nil {
		return nil, err
	}
	// Write call notification to new alert characteristic
	err := i.newAlertChar.WriteValue(
		append([]byte{0x03, 0x01, 0x00}, []byte(from)...),
		nil,
	)
	if err != nil {
		return nil, err
	}

	if !i.notifEventDone {
		err = i.initNotifEvent()
		if err != nil {
			return nil, err
		}
		i.notifEventDone = true
	}

	return i.notifEventCh, nil
}

// initNotifEvent initializes the notification event channel
func (i *Device) initNotifEvent() error {
	// Start notifications on notification event characteristic
	err := i.notifEventChar.StartNotify()
	if err != nil {
		return err
	}
	// Watch properties of notification event characteristic
	ch, err := i.notifEventChar.WatchProperties()
	if err != nil {
		return err
	}
	// Create new output channel for status
	i.notifEventCh = make(chan uint8, 1)
	go func() {
		// For every event
		for event := range ch {
			// If value changed
			if event.Name == "Value" {
				// Send status to channel
				i.notifEventCh <- uint8(event.Value.([]byte)[0])
			}
		}
	}()
	return nil
}

// FS creates and returns a new filesystem from the device
func (i *Device) FS() (*blefs.FS, error) {
	if err := i.checkStatus(i.fsTransferChar, FSTransferChar); err != nil {
		return nil, err
	}
	return blefs.New(i.fsTransferChar)
}

// AddWeatherEvent adds one of the event structs from
// the weather package to the timeline. Input must be
// a struct containing TimelineHeader.
func (i *Device) AddWeatherEvent(event interface{}) error {
	if err := i.checkStatus(i.weatherDataChar, WeatherDataChar); err != nil {
		return err
	}
	// Get type of input
	inputType := reflect.TypeOf(event)
	// Check if input contains TimelineHeader
	_, hdrExists := inputType.FieldByName("TimelineHeader")
	// If header does not exist or input is not struct
	if !hdrExists || inputType.Kind() != reflect.Struct {
		return ErrNoTimelineHeader
	}

	// Encode event as CBOR
	data, err := cbor.Marshal(event)
	if err != nil {
		return err
	}

	log.Debug().Interface("event", event).Msg("Adding weather event")
	// Write data to weather data characteristic
	return i.weatherDataChar.WriteValue(data, nil)
}

func (i *Device) checkStatus(char *gatt.GattCharacteristic1, uuid string) error {
	log.Debug().Msg("Checking characteristic status")
	connected, err := i.device.GetConnected()
	if err != nil {
		return err
	}
	if !connected {
		return ErrNotConnected
	}
	if char == nil {
		log.Debug().Msg("Characteristic not available (nil)")
		return ErrCharNotAvail{uuid}
	}
	log.Debug().
		Str("uuid", char.Properties.UUID).
		Str("name", charNames[char.Properties.UUID]).
		Msg("Characteristic available")
	return nil
}