#include "obus_can.h"
#include "obus_module.h"

#define PIN_LED_RED 4
#define PIN_LED_GREEN 7

#define BLINK_DELAY_SLOW 1000
#define BLINK_DELAY_FAST 300

#define MAX_TIME_BETWEEN_CALLS 100

#define COLOR_OFF    ((struct color) {false, false})
#define COLOR_RED    ((struct color) {true,  false})
#define COLOR_GREEN  ((struct color) {false, true})
#define COLOR_YELLOW ((struct color) {true,  true})

namespace obus_module {

struct obus_can::module this_module;
uint8_t strike_count;
bool active;
bool acked_after_last_hello = false;
uint32_t next_loop_call_deadline;

// Current LED status
struct color { bool red; bool green; };
struct color led_color;
//   Keeps track of whether the LED is currently lit, when a blink pattern is active
bool blink_led_lit = false;
int blink_delay = 0;
unsigned long blink_next_time = 0;
uint32_t led_reset_time;


void _setLed(struct color color) {
	led_color = color;
	blink_delay = 0;
	led_reset_time = 0;

	digitalWrite(PIN_LED_RED, color.red ? HIGH : LOW);
	digitalWrite(PIN_LED_GREEN, color.green ? HIGH : LOW);
}

void _ledLoop() {
	// Check if we need to turn the LED back off, e.g. to reset the strike blinker
	if (led_reset_time && millis() > led_reset_time) {
		if (active) {
			_setLed(COLOR_YELLOW);
		} else {
			_setLed(COLOR_OFF);
		}
		led_reset_time = 0;
	}

	// Update blink of status LED
	if (blink_delay && millis() > blink_next_time) {
		blink_led_lit = !blink_led_lit;
		if (blink_led_lit) {
			digitalWrite(PIN_LED_RED, led_color.red ? HIGH : LOW);
			digitalWrite(PIN_LED_GREEN, led_color.green ? HIGH : LOW);
		} else {
			digitalWrite(PIN_LED_RED, false);
			digitalWrite(PIN_LED_GREEN, false);
		}

		blink_next_time = millis() + blink_delay;
	}
}

void _setLedBlink(struct color color, uint16_t delay) {
	led_color = color;
	blink_led_lit = false;
	blink_delay = delay;
	blink_next_time = millis();
	led_reset_time = 0;

	_ledLoop();
}

void blink_error(String message) {
	bool blink = false;
	while (true) {
		digitalWrite(PIN_LED_RED, blink);
		digitalWrite(PIN_LED_GREEN, blink);
		blink = !blink;
		delay(blink ? BLINK_DELAY_SLOW : BLINK_DELAY_FAST);
		Serial.println(message);
	}
}

void _resetState() {
	strike_count = 0;
	active = false;
	next_loop_call_deadline = 0;
	acked_after_last_hello = false;

	if (this_module.type == OBUS_TYPE_PUZZLE || this_module.type == OBUS_TYPE_NEEDY) {
		pinMode(PIN_LED_RED, OUTPUT);
		pinMode(PIN_LED_GREEN, OUTPUT);

		_setLedBlink(COLOR_GREEN, BLINK_DELAY_SLOW);
	}
}

void setup(uint8_t type, uint8_t id) {
	this_module.type = type;
	this_module.id = id;
	_resetState();

	if (!obus_can::init()) {
		blink_error(F("CAN init failed"));
	}
}

void empty_callback_info(uint8_t info_id, uint8_t infomessage[7]) {
	// Mark arguments as not used
	(void)info_id;
	(void)infomessage;
}

void empty_callback_state(uint32_t time_left, uint8_t strikes, uint8_t max_strikes, uint8_t puzzle_modules_left) {
	// Mark arguments as not used
	(void)time_left;
	(void)strikes;
	(void)max_strikes;
	(void)puzzle_modules_left;
}

bool loopPuzzle(obus_can::message* message, void (*callback_game_start)(uint8_t puzzle_modules), void (*callback_game_stop)(), void (*callback_info)(uint8_t info_id, uint8_t infomessage[7]), void (*callback_state)(uint32_t time_left, uint8_t strikes, uint8_t max_strikes, uint8_t puzzle_modules_left)) {
	// TODO this can be more efficient by only enabling error interrupts and
	//  reacting to the interrupt instead of checking if the flag is set in a loop
	// We will need to fork our CAN library for this, because the needed functions are private.
	// Also, we can't do this by default, because the INT pin is normally not connected to the board
	if (obus_can::is_error_condition()) {
		blink_error(F("E CAN error"));
	}

	// Force the user of the library to periodically call loop
	if (next_loop_call_deadline != 0 && millis() > next_loop_call_deadline) {
		blink_error(F("E Missed loop deadline"));
	}
	next_loop_call_deadline = millis() + MAX_TIME_BETWEEN_CALLS;

	bool received_message = false;
	if (obus_can::receive(message)) {
		received_message = true;
		if (is_from_controller(message->from)) {
			uint32_t seed;
			switch (message->msg_type) {
				case OBUS_MSGTYPE_C_GAMESTART:
					if (acked_after_last_hello) {
						active = true;
						_setLed(COLOR_YELLOW);
						callback_game_start(message->gamestatus.puzzle_modules_left);
					}
					break;
				case OBUS_MSGTYPE_C_HELLO:
					_resetState();
					obus_can::send_m_hello(this_module);
					break;
				case OBUS_MSGTYPE_C_SOLVED:
				case OBUS_MSGTYPE_C_TIMEOUT:
				case OBUS_MSGTYPE_C_STRIKEOUT:
					if (acked_after_last_hello) {
						active = false;
						_setLed(COLOR_OFF);
						callback_game_stop();
					}
					break;
				case OBUS_MSGTYPE_C_ACK:
					if (message->payload_address.type == this_module.type && message->payload_address.id == this_module.id) {
						acked_after_last_hello = true;
					}
					break;
				case OBUS_MSGTYPE_C_STATE:
					callback_state(message->gamestatus.time_left, message->gamestatus.strikes, message->gamestatus.max_strikes, message->gamestatus.puzzle_modules_left);
					break;
				case OBUS_MSGTYPE_C_INFOSTART:
					// Add module type and id to seed, to remove correlation in randomness between modules
					seed = message->infostart.seed + ((uint32_t) this_module.type << 8) + ((uint32_t) this_module.id);
					// randomSeed has no effect when called with 0 as seed, so we use
					//  a fallback value that is unlikely to collide with other frequently used seeds
					if (seed == 0) {
						seed--;
					}
					randomSeed(seed);
					break;
				default:
					break;
			}
		} else if (message->from.type == OBUS_TYPE_INFO) {
			uint8_t infobuffer[7] = {0};
			memcpy(infobuffer, message->infomessage.data, message->infomessage.len);
			callback_info(message->from.id, infobuffer);
		}
	}

	_ledLoop();

	return received_message;
}

bool loopNeedy(obus_can::message* message, void (*callback_game_start)(uint8_t puzzle_modules), void (*callback_game_stop)(), void (*callback_info)(uint8_t info_id, uint8_t infomessage[7]), void (*callback_state)(uint32_t time_left, uint8_t strikes, uint8_t max_strikes, uint8_t puzzle_modules_left)) {
	// For now this is the same function
	return loopPuzzle(message, callback_game_start, callback_game_stop, callback_info, callback_state);
}

bool loopInfo(obus_can::message* message, int (*info_generator)(uint8_t*)) {
	bool interesting_message = false;
	if (obus_can::receive(message)) {
		if (is_from_controller(message->from)) {
			switch (message->msg_type) {
				case OBUS_MSGTYPE_C_INFOSTART:
					{
						randomSeed(message->infostart.seed);
						uint8_t info_message[OBUS_PAYLD_INFO_MAXLEN];
						int len = info_generator(info_message);
						obus_can::send_i_infomessage(this_module, info_message, len);
					}
					break;
				case OBUS_MSGTYPE_C_STATE:
					interesting_message = true;
					break;
				default:
					break;
			}
		}
	}
	return interesting_message;
}

void strike() {
	if (!active) {
		return;
	}
	strike_count++;
	_setLedBlink(COLOR_RED, BLINK_DELAY_FAST);
	led_reset_time = millis() + 2000;
	obus_can::send_m_strike(this_module, strike_count);
}

void solve() {
	if (!active) {
		return;
	}
	obus_can::send_m_solved(this_module);
	active = false;
	_setLed(COLOR_GREEN);
}

bool is_active() {
	return active;
}

}
