package main

// BLE scanner example.
// Scans for nearby BLE devices and shows their names and signal strength
// (RSSI) on the display. Press button A to clear the list.

import (
	"image/color"
	"machine"
	"strconv"
	"time"

	"tinygo.org/x/bluetooth"
	"tinygo.org/x/drivers/st7789"
	"tinygo.org/x/tinyfont"
	"tinygo.org/x/tinyfont/freesans"
)

const maxDevices = 5
const maxNameLen = 16
const scanWindow = 500 * time.Millisecond

// Fixed-size byte arrays avoid dangling pointers into SoftDevice buffers.
type bleDevice struct {
	name [maxNameLen]byte
	nLen uint8
	addr bluetooth.Address
	rssi int16
}

var (
	adapter = bluetooth.DefaultAdapter
	display st7789.Device
	devices [maxDevices]bleDevice
	count   int
)

var (
	black  = color.RGBA{0, 0, 0, 255}
	cyan   = color.RGBA{0, 200, 255, 255}
	white  = color.RGBA{255, 255, 255, 255}
	yellow = color.RGBA{255, 220, 0, 255}
	green  = color.RGBA{0, 220, 100, 255}
	gray   = color.RGBA{150, 150, 150, 255}
)

func main() {
	time.Sleep(2 * time.Second)
	initDisplay()

	btnA := machine.P1_06
	btnA.Configure(machine.PinConfig{Mode: machine.PinInputPullup})

	must("enable BLE", adapter.Enable())
	drawHeader()

	for {
		if !btnA.Get() {
			count = 0
			time.Sleep(200 * time.Millisecond)
		}

		// adapter.Scan never yields to TinyGo's scheduler, so a goroutine
		// calling StopScan() never gets CPU time.
		// Fix: check elapsed time inside the callback and stop from there.
		scanStart := time.Now()
		adapter.Scan(func(a *bluetooth.Adapter, result bluetooth.ScanResult) {
			if time.Since(scanStart) >= scanWindow {
				adapter.StopScan()
				return
			}
			for i := 0; i < count; i++ {
				if devices[i].addr == result.Address {
					devices[i].rssi = result.RSSI
					return
				}
			}
			if count >= maxDevices {
				return
			}
			n := result.LocalName()
			if n == "" {
				n = result.Address.String()
			}
			nLen := len(n)
			if nLen > maxNameLen {
				nLen = maxNameLen
			}
			copy(devices[count].name[:], n)
			devices[count].nLen = uint8(nLen)
			devices[count].addr = result.Address
			devices[count].rssi = result.RSSI
			count++
		})

		// Scan has stopped — safe to use SPI now.
		drawDevices()
	}
}

func initDisplay() {
	machine.SPI0.Configure(machine.SPIConfig{
		SCK:       machine.P1_01,
		SDO:       machine.P1_02,
		Frequency: 8000000,
		Mode:      0,
	})
	display = st7789.New(machine.SPI0,
		machine.P1_15, machine.P1_13, machine.P0_10, machine.P0_09)
	display.Configure(st7789.Config{
		Rotation:     st7789.ROTATION_90,
		Width:        135,
		Height:       240,
		RowOffset:    40,
		ColumnOffset: 53,
	})
	display.FillScreen(black)
}

func drawHeader() {
	tinyfont.WriteLine(&display, &freesans.Regular9pt7b, 10, 24, "BLE Scanner", cyan)
}

func drawDevices() {
	display.FillRectangle(0, 32, 240, 103, black)

	if count == 0 {
		tinyfont.WriteLine(&display, &freesans.Regular9pt7b, 10, 55, "No devices found...", gray)
		return
	}

	for i := 0; i < count; i++ {
		y := int16(52 + i*20)
		name := string(devices[i].name[:devices[i].nLen])
		rssi := " " + strconv.Itoa(int(devices[i].rssi)) + "dB"

		c := white
		if devices[i].rssi > -60 {
			c = green
		} else if devices[i].rssi > -80 {
			c = yellow
		}

		tinyfont.WriteLine(&display, &freesans.Regular9pt7b, 5, y, name+rssi, c)
	}
}

func must(action string, err error) {
	if err != nil {
		panic("failed to " + action + ": " + err.Error())
	}
}
