'''
License
-------
No Copyright

The person who associated a work with this deed has dedicated the work to the
public domain by waiving all of his or her rights to the work worldwide under
copyright law, including all related and neighboring rights, to the extent
allowed by law.

You can copy, modify, distribute and perform the work, even for commercial
purposes, all without asking permission.

See https://creativecommons.org/share-your-work/public-domain/cc0/ for more
information on the license.


Project
-------
This file is part of the wedding pictogram project, made by
Maarten Tromp <maarten@geekabit.nl>.

More info on the project:
https://www.geekabit.nl/projects/wedding-gift-sprite/
'''


from micropython import const
from neopixel import NeoPixel
from bluetooth import BLE
import machine
import time
import random
import struct

import config # read icon configuration 

# global constants for demo
_number_of_leds = const(24)	# number of led pixels
_max_brightness = const(10)	# max brighness

# global vars for demo
np = NeoPixel(machine.Pin(15, machine.Pin.OUT), _number_of_leds)	# create array of pixels

# vars for outlineRunner demo
pix_loc = [-2] * 6
pix_col = [0] * 6
pix_speed = [1] * 6

# constants for bluetooth
_ADV_TYPE_FLAGS = const(0x01)
_ADV_TYPE_UUID16_COMPLETE = const(0x3)
_ADV_TYPE_UUID32_COMPLETE = const(0x5)
_ADV_TYPE_UUID128_COMPLETE = const(0x7)
_ADV_TYPE_NAME = const(0x09)
_IRQ_SCAN_RESULT = const(0x05)

_rssi_close = const(-80)
_rssi_halfway = const(-100)

# vars for distance sensing and demo syncing
_updated = False
_distance = 0
_last_seen = 0


# common functions
def advertising_payload():
	payload = bytearray()

	def _append(adv_type, value):
		nonlocal payload
		payload += struct.pack("BB", len(value) + 1, adv_type) + value

	_append(_ADV_TYPE_FLAGS, struct.pack("B", 0x06))
	# TODO list of services
	_append(_ADV_TYPE_NAME, config.me)
	return payload

def adv_decode(adv_type, data):
	i = 0
	while i + 1 < len(data):
		if data[i + 1] == adv_type:
			return data[i + 2:i + data[i] + 1]
		i += 1 + data[i]
	return None

def adv_decode_name(data):
	n = adv_decode(_ADV_TYPE_NAME, data)
	if n:
		return n.decode('utf-8')
	return ""

def int_ble(event, data):
	# This interrupt fires whan a bluetooth event happens, such as a scan result or connect.
	# Keep interrupt routines as short as possible.  https://docs.micropython.org/en/latest/reference/isr_rules.html#isr-rules  also no floating point ops

	if event == _IRQ_SCAN_RESULT:
		# A single scan result.
		addr_type, addr, adv_type, rssi, adv_data = data
		name = adv_decode_name(adv_data)
		#print("scan result: addr_type=%i, addr=%s, adv_type=%i, rssi=%i, name=%s" % (addr_type, binascii.hexlify(addr), adv_type, rssi, adv_decode_name(adv_data)))
		if name == config.spouse:
			#print("rssi: %i" % rssi)
			global _distance, _last_seen, _updated

			# calculate distance
			if rssi > _rssi_close:
				_distance = 0 # close
			elif rssi > _rssi_halfway:
				_distance = 1 # halfway
			else:
				_distance = 2 # far
			# TODO maybe some averaging?

			# reset timer
			_last_seen = 0

			_updated = True


# timer interrupt
def int_timer(timer):
	global _last_seen, _distance, _updated
	_last_seen += 1
	if _last_seen == 3:
		# missed at least 2 broadcasts
		_distance = 2 # far
		_updated = True


def wheel(wheelPos):
	# colour wheel: red -> orange -> yellow -> green -> blue -> purple -> red
	# input: wheelPos = 0..255
	# output: r, g, b values
	wheelPos = wheelPos % 256
	r = 0
	g = 0
	b = 0
	if wheelPos < 42:
		# red - orange
		i = wheelPos / 42
		r = _max_brightness
		g = int(i * _max_brightness / 4)
		return r, g, b
	if wheelPos < 84:
		# orange - yellow
		wheelPos -= 42
		i = wheelPos / 42
		r = _max_brightness
		g = int((1 - i) * _max_brightness / 4 + i * 3 / 4 * _max_brightness)
		return r, g, b
	if wheelPos < 127:
		# yellow - green
		wheelPos -= 84
		i = wheelPos / 42
		r = int((1 - i) * _max_brightness)
		g = int((1 - i) * 3 / 4 * _max_brightness + i * _max_brightness)
		return r, g, b
	if wheelPos < 170:
		# green - blue
		wheelPos -= 127
		i = wheelPos / 42
		g = int((1 - i) * _max_brightness)
		b = int(i * _max_brightness)
		return r, g, b
	if wheelPos < 212:
		# blue - purple
		wheelPos -= 170
		i = wheelPos / 42
		r = int(i * _max_brightness)
		b = _max_brightness
		return r, g, b
	# purple - red
	wheelPos -= 212
	r = _max_brightness
	b = int((1 - wheelPos / 42) * _max_brightness)
	return r, g, b


# all demos use 2 global variables:
#	distance: 0 = close, 1=halfway, 2=far
#	ticks: counter
# return values:
#	done: true when this demo is complete

def ledTest(distance, ticks):
	if distance == 0: # close
		# 7 colours
		colours = [[_max_brightness, 0, 0], [0, _max_brightness, 0], [0, 0, _max_brightness], [_max_brightness, _max_brightness // 4, 0], [_max_brightness, _max_brightness, 0], [_max_brightness, 0, _max_brightness], [0, _max_brightness, _max_brightness]]
	elif distance == 1: # halfway
		# whitish color, black
		#colours = [[_max_brightness, 0, 0], [_max_brightness, _max_brightness, _max_brightness], [0, 0, 0], [_max_brightness, _max_brightness, 0], [_max_brightness, _max_brightness, _max_brightness], [0, 0, 0], [0, 0, _max_brightness], [_max_brightness, _max_brightness, _max_brightness], [0, 0, 0]]
		colours = [[_max_brightness, _max_brightness // 4, _max_brightness // 4], [0, 0, 0], [_max_brightness, _max_brightness, _max_brightness // 4], [0, 0, 0], [_max_brightness // 4, _max_brightness // 4, _max_brightness], [0, 0, 0]]
	else: # far
		# white, black
		colours = [[_max_brightness, _max_brightness, _max_brightness], [0, 0, 0]]

	colourIndex = (ticks // _number_of_leds) % len(colours)
	step = ticks % _number_of_leds

	np[step] = colours[colourIndex]
	if (colourIndex == len(colours) - 1 and step == _number_of_leds - 1):
		return True


def whiteOverRainbow(distance, ticks):
	# inspired by particle xmas tree demo
	# https://github.com/particle-iot/xmastree/blob/master/src/firmware/animations.h

	whiteLength = const(3)
	rainbowSpeed = const(4)

	if distance == 0: # close
		maxRounds = 4
	elif distance == 1: # halfway
		maxRounds = 3
	else: # far
		maxRounds = 2

	headPos = (whiteLength - 1 + ticks) % _number_of_leds
	tailPos = ticks % _number_of_leds
	rainbowPos = (rainbowSpeed * ticks) % 256
	rounds = ticks / _number_of_leds
	for i in range(_number_of_leds):
		if ((i >= tailPos and i <= headPos) or (tailPos > headPos and i >= tailPos) or (tailPos > headPos and i <= headPos)):
			np[i] = (_max_brightness, _max_brightness, _max_brightness)
		else:
			pos = int(256 * (i / _number_of_leds + rainbowPos / 256))
			r, g, b = wheel(pos)
			if distance == 0: # close
				# rainbow background
				np[i] = (r, g, b)
			elif distance == 1: # halfway 
				# dimmed rainbow background
				np[i] = (r // 4, g // 4, b // 4)
			else: # far
				# black background
				np[i] = (0, 0, 0)
	if rounds > maxRounds:
		return True


def chasingPixels(distance, ticks):
	speeds = [-8, -5, -3, -2, -1, 1, 2, 3, 5, 8]
	outline = [1, 2, 3, 4, 7, 8, 9, 10, 12, 13, 14, 15, 16, 17, 20, 21, 22, 23]

	# on first tick, init global vars
	# does not seem to work though
#	if ticks == 0:
#		pix_loc = [-2] * 6 # current location
#		pix_col = [0, 0, 0] * 6 # colour
#		pix_speed = [1] * 6 # number of ticks to wait between steps

	if distance == 0: # close
		# 7 colours
		numPix = 4
		maxTicks = 200
		colours = [[_max_brightness, 0, 0], [0, _max_brightness, 0], [0, 0, _max_brightness], [_max_brightness, _max_brightness // 4, 0], [_max_brightness, _max_brightness, 0], [_max_brightness, 0, _max_brightness], [0, _max_brightness, _max_brightness]]
	elif distance == 1: # halfway
		# 3x colour, 3x white
		numPix = 3
		maxTicks = 150
		colours = [[_max_brightness, 0, 0], [_max_brightness, _max_brightness, _max_brightness], [_max_brightness, _max_brightness, 0], [_max_brightness, _max_brightness, _max_brightness], [0, 0, _max_brightness], [_max_brightness, _max_brightness, _max_brightness]]
	else: # far
		# white
		numPix = 2
		maxTicks = 100
		colours = [[_max_brightness, _max_brightness, _max_brightness]]

	# clear all leds
	for i in range(_number_of_leds):
		np[i] = (0, 0, 0)

	# advance pixels
	for p in range(numPix):
		# test if it's time for next step
		if (ticks % pix_speed[p] == 0):
			# find out direction
			if pix_speed[p] > 0:
				pix_loc[p] += 1
			else:
				pix_loc[p] -= 1
			# test for bounds
			if (pix_loc[p] < 0 or pix_loc[p] > len(outline)):
				# test if we're done
				if ticks > maxTicks:
					return True
				# re-initialize pixel
				pix_speed[p] = random.choice(speeds)
				if pix_speed[p] > 0:
					pix_loc[p] = 0 # start at bottom
				else:
					pix_loc[p] = len(outline) # start at top
				pix_col[p] = random.randrange(len(colours))
		# draw pixels
		for i in range(len(outline)):
			if pix_loc[p] == i:
				pix_col[p] %= len(colours) # distance might have changed
				# add pixel colour to alreacy existing led colour
				r = min(np[outline[i]][0] + colours[pix_col[p]][0], _max_brightness)
				g = min(np[outline[i]][1] + colours[pix_col[p]][1], _max_brightness)
				b = min(np[outline[i]][2] + colours[pix_col[p]][2], _max_brightness)
				np[outline[i]] = (r, g, b)


def fill(distance, ticks):
	# orders differ a bit between bride and groom boards
	if config.board_type == 0:
		# groom
		vert = [[12, 13], [11], [10, 14], [9, 15], [6, 18], [8, 16], [5, 19], [0, 7, 17], [4, 20], [3, 21], [2, 22], [1, 23]] # 12 long
		horiz = [[7, 8, 9], [1, 2, 3, 4, 5, 6, 10, 12], [0, 11], [13, 14, 18, 19, 20, 21, 22, 23], [15, 16, 17]] # 5 long
	else:
		# bride
		vert = [[12, 13], [11], [10, 14], [9, 15], [6, 18], [8, 16], [5, 19], [7, 17], [4, 20], [3, 21], [0, 2, 22], [1, 23]] # 12 long
		horiz = [[2, 7], [3, 8], [4, 9], [1, 5, 6, 10, 12], [0, 11], [13, 14, 18, 19, 23], [15, 20], [16, 21], [17, 22]] # 9 long
	# same colours as ledTest
	if distance == 0: # close
		# 7 colours
		maxTicks = 250
		colours = [[_max_brightness, 0, 0], [0, _max_brightness, 0], [0, 0, _max_brightness], [_max_brightness, _max_brightness // 4, 0], [_max_brightness, _max_brightness, 0], [_max_brightness, 0, _max_brightness], [0, _max_brightness, _max_brightness]]
	elif distance == 1: # halfway
		# color, white, black
		maxTicks = 200
		colours = [[_max_brightness, 0, 0], [_max_brightness, _max_brightness, _max_brightness], [0, 0, 0], [_max_brightness, _max_brightness, 0], [_max_brightness, _max_brightness, _max_brightness], [0, 0, 0], [0, 0, _max_brightness], [_max_brightness, _max_brightness, _max_brightness], [0, 0, 0]]
	else: # far
		# white, black
		maxTicks = 150
		colours = [[_max_brightness, _max_brightness, _max_brightness], [0, 0, 0]]
	random = [124, 140, 79, 19, 25, 113, 135, 130, 202, 148, 6] # https://xkcd.com/221/
	step = ticks % 30
	colourIndex = ticks // 30 % len(colours)
	dir = random[ticks // 30 % len(random)] % 4
	line = []
	if dir == 0:
		# top to bottom
		if step < len(vert):
			line = vert[step]
		if step > 19:
			ticks += 10
	elif dir == 1:
		# bottom to top
		if step < len(vert):
			line = vert[len(vert) - step - 1]
		if step > 19:
			ticks += 10
	elif dir == 2:
		# left to right
		if True: #config.isServer:
			# FIXME
			# server shows step 0..9
			if step < len(horiz):
				line = horiz[step]
		else:
			# client shows step 10..19
			if step > 9:
				step -= 10
				if step < len(horiz):
					line = horiz[step]
	else:
		# right to left
		if True: #config.isServer:
			# FIXME
			# server shows step 0..9
			if step > 9:
				step -= 10
				if step < len(horiz):
					line = horiz[len(horiz) - 1 - step]
		else:
			# client shows step 10..19
			if step < len(horiz):
				line = horiz[len(horiz) - 1 - step]
	for pixel in line:
		np[pixel] = colours[colourIndex]
	if (ticks > maxTicks and step == 19):
		return True


def pink(distance, ticks):
	# inspired by pink wedding invitation
	# TODO add some twinkling

	# set base colour
	if distance == 0:
		# close -> pink
		r = _max_brightness
		g = int(_max_brightness / 2.5)
		b = int(_max_brightness / 2.5)
	for i in range(_number_of_leds):
		np[i] = (r, g, b)
	return True


def boringBreathe(distance, ticks):
	wait = 50

	while True:
		for j in range(2 * _max_brightness - 1):
			if (j < _max_brightness + 1):
				v = j
			else:
				v = 2 * _max_brightness - j
			for i in range(l):
				np[i] = (v, v, v)
			np.write()
			time.sleep_ms(wait)


#def defeest():
	# 2 blue and 5 yellow pixels


#def l():
	# make [L] sign: blue arm and upper body blue, rest yellow


# could do fire effect


# main function
def main():
	global _updated, _distance

	print("main start")

	distance = 0 # 0=close, 1=halfway, 2=far
	ticks = 0
	demos = {
		0: whiteOverRainbow,
		1: chasingPixels,
		2: fill,
		3: ledTest,
	}
	demo_index = 0

	# set a lovely pink waiting screen since ble init can take a while
	r = _max_brightness
	g = int(_max_brightness / 2.5)
	b = int(_max_brightness / 2.5)
	for i in range(_number_of_leds):
		np[i] = (r, g, b)
	np.write()

	# button
	button = machine.Pin(0, machine.Pin.IN, machine.Pin.PULL_UP)
	lastButtonValue = 1

	# configure BLE
	ble = BLE()
	ble.active(True)

	# start advertising
	payload = advertising_payload()
	ble.gap_advertise(1000000, adv_data = payload, connectable = False) # interval in us => 1s

	# start scanning
	ble.irq(handler = int_ble)
	ble.gap_scan(0, 30000, 30000) # continuously
	# will update some global variables when interrupt fires

	# configure timer interrupt
	# should fire as often as bluetooth broadcast
	timer = machine.Timer(0)
	timer.init(period = 1000, callback = int_timer) # period in ms => 1s

	print("init done")

	# main loop
	while True:
		# read button
		buttonValue = button.value()
		if buttonValue == 0 and lastButtonValue == 1: # falling edge
			#distance = (distance + 1) % 3
			break
		lastButtonValue = buttonValue

		# call demo
		demo_finished = demos[demo_index](distance, ticks)
		# advance to next step in demo
		ticks += 1
		if demo_finished:
			# advance to next demo
			demo_index = (demo_index + 1) % len(demos)
			ticks = 0

		# update leds
		# this is a timing critical section, disable interrupts
		irq_state = machine.disable_irq()
		np.write()

		# update demo variables
		# copy value from global variable instead of updating the variables directly during ble interrupt. Otherwise values could be chaged in the midlde of demo code.
		# disable interrupts during copying.
		if _updated:
			_updated = False
			distance = _distance

		# critical section is over, enable interrupts again
		machine.enable_irq(irq_state) 

		# sleep is distance dependant
		if distance == 0: # close
			sleep = 50
		elif distance == 1: # halfway
			sleep = 100
		else: # far
			sleep = 200
		time.sleep_ms(sleep)

	# disable timer
	timer.deinit()

	# stop advertising
	ble.gap_advertise(None)

	# stop scanning
	ble.gap_scan(None)

	# disable ble
	ble.active(False)

main()


# end of file
