/*
 * display.cpp
 *
 *  Created on: Sep 26, 2017
 *      Author: Philipp Hinz
 */
#include <stdlib.h>
#include <pthread.h>
#include <time.h>
#include <unistd.h>
#include <string.h>

#include "global.h"
#include "display.h"
#include "logger.h"
#include "database.h"
#include "timer.h"
#include "lcd.h"
#include "coffee.h"
#include "hal.h"
#include "events.h"
#include "DS18B20.h"

display_lang_t displayLang;
timer displayTimer(displayTimerHandler);
int lcd = 0;
volatile int timerScaler = 0;
volatile int elapsedCnt = 0;
coffee_status_t coffeeState = STATE_OFF;
coffee_mode_t coffeeMode = MODE_STATE;
coffee_menuPage_t coffeePage = PAGE_SOFTOFF;
bool coffeeDescaling = false;

/**
 * Prints out the current time in a centered position
 * @param line Target line in display
 */
void displayPrintTime(int line) {
	time_t rawtime;
	struct tm * timeinfo;
	if (line > DISPLAY_ROWS)
		line = 0;

	time(&rawtime);
	timeinfo = localtime(&rawtime);
	lcdPosition(lcd, 0, line);
	lcdPrintf(lcd, "    %.2d:%.2d:%.2d    ", timeinfo->tm_hour,
			timeinfo->tm_min, timeinfo->tm_sec);
}

/**
 * Prints out the current temperature in a centered position
 * @param line Target line in display
 */
void displayPrintTemp(int line) {
	//TODO: the temperature is wrong(old value) the first time the page is opened
	//after the first refresh the right temperature is displayed
	//no idea where this comes from, datasheet says nothing about dummy readouts.
	//no old values are used, conversion is started before every readout...
	if (line > DISPLAY_ROWS)
		line = 0;

	lcdPosition(lcd, 0, line);
	lcdPrintf(lcd, "       %d C      ", DS18B20_readTemp());
}
/**
 * Prints the total epsressi brewed since reset
 */
void displayPrintStats(int line) {
	char buffer[17];
	if (line > DISPLAY_ROWS)
			line = 0;

	lcdPosition(lcd, 0, line);
	sprintf(buffer, "%7d espressi", getBrewCounter());
	lcdPrintf(lcd, buffer);
}
/**
 * Prints the total heating time in kWh
 */
void displayPrintStats2(int line) {
	char buffer[17];
	if (line > DISPLAY_ROWS)
		line = 0;

	uint64_t totalkWh = 2 * getTotalHeatingTime()/(60*60);
	sprintf(buffer, "%lld kWh", totalkWh);
	displayPrintLn(line, buffer, true);
}

/**
 * Prints the remaining time or the remaining cups until the next descaling is necessary
 */
void displayPrintNextDesc(int line) {
	char buffer[17];
	if (line > DISPLAY_ROWS)
		line = 0;

	sprintf(buffer, "%d d or %d C", checkDirtyTime(), checkDirtyEspresso());
	displayPrintLn(line, buffer, true);
}

/**
 * Prints out the total volume flow
 * @param line Target line in display
 */
void displayPrintFlow(int line) {
	float flow = halGetFlow();
	lcdPosition(lcd, 0, line);
	lcdPrintf(lcd, "%s: %.0f ml    ", displayGetString(str_flow), flow);
}

/**
 * Prints a string to a specific line, optionally centered.
 * This function also fills out the remaining row of the display with spaces,
 * to ensure there is no old data left.
 * @param line Target line in display
 * @param *str String to print
 * @param centered Print centered or not
 */

void displayPrintLn(int line, const char* str, bool centered) {
	char buf[DISPLAY_COLS + 1];
	int len = strlen(str);
	int i = 0;
	int spaces = 0;
	if (len > DISPLAY_COLS)
		len = DISPLAY_COLS;
	if (line > DISPLAY_ROWS)
		line = 0;
	if (centered) {
		spaces = (DISPLAY_COLS - len) / 2;
		if (spaces) {
			for (i = 0; i < spaces; i++) {
				buf[i] = ' ';
			}
		}
	}
	for (i = 0; i < len; i++) {
		buf[spaces + i] = str[i];
	}
	if ((len + spaces) < DISPLAY_COLS) { // fill remaining space
		for (i = i + spaces; i < DISPLAY_COLS; i++) {
			buf[i] = ' ';
		}
	}
	//draw the descaling star
	if(coffeeDescaling && line == 0){
		buf[15] = '*';
	}
	lcdPosition(lcd, 0, line);
	buf[DISPLAY_COLS] = '\0';
	lcdPrintf(lcd, buf);
	//logger(V_HAL, "Printed out on display: \"%s\"\n", buf);
}

/**
 * Updates the display state to the matching coffee state
 * @param event Event data
 */

void displayStateUpdated(event_t *event) {
	if (event->len != sizeof(coffee_status_t)) {
		logger_error("Invalid use of event %s\n", event->event);
		return;
	}
	coffee_status_t state = *(coffee_status_t*) event->data;
	if (state != coffeeState) {
		coffeeState = state;
		if (timerScaler)
			timerScaler--;
		elapsedCnt = 0;
		displayTimer.call();
	}
}

/**
 * Updates the display state to the matching coffee display mode
 * @param event Event data
 */

void displayModeUpdated(event_t *event) {
	if (event->len != sizeof(coffee_mode_t)) {
		logger_error("Invalid use of event %s\n", event->event);
		return;
	}
	coffee_mode_t mode = *(coffee_mode_t*) event->data;
	if (mode != coffeeMode) {
		coffeeMode = mode;
		if (timerScaler)
			timerScaler--;
		elapsedCnt = 0;
		displayTimer.call();
	}
}

/**
 * Updates the display state to the matching coffee display page
 * @param event Event data
 */

void displayPageUpdated(event_t *event) {
	if (event->len != sizeof(coffee_menuPage_t)) {
		logger_error("Invalid use of event %s\n", event->event);
		return;
	}
	coffee_menuPage_t page = *(coffee_menuPage_t*) event->data;
	if (page != coffeePage) {
		coffeePage = page;
		timerScaler = REFRESH_RATE;
		elapsedCnt = 0;
		displayTimer.call();
	}
}

/**
 * Updates the current descaling status of the machine
 */
void displayDescaling(event_t *event) {
	if (event->len != sizeof(bool)) {
		logger_error("Invalid use of event %s\n", event->event);
		return;
	}
	coffeeDescaling = *(bool*) event->data;
}

/**
 * Prints the logo (CoffeePi) and also the temperature with the logo (CoffeePi@72C) if the machine is on
 */
void displayPrintLogo(void) {
	char buffer[17];
	switch (coffeeState) {
		case STATE_HEATING:
		case STATE_INITALHEATING:
		case STATE_IDLE:
			sprintf(buffer, "CoffeePi @ %dC", DS18B20_readTemp());
			break;
		default:
			sprintf(buffer, "CoffeePi");
			break;
	}
	displayPrintLn(0, buffer, true);
}

/**
 * Handles cleanup before program termination
 */

void displayTerminate(event_t *e) {
	logger(V_BASIC, "display.cpp: Terminating\n");
	displayPrintLn(0, "CoffeePi", true);
	displayPrintLn(1, displayGetString(str_bye), true);
}

/**
 * Main thread to handle display data
 * @param *threadid Thread ID
 */

void* displayThread(void* threadid) {
	//sleep(1); // Wait for other components to initialize
	displayTimer.start();
	while (1) {
		pause();
		if (1) {
			timerScaler = REFRESH_RATE;
			displayTimer.call();
		}
	}
	pthread_exit(EXIT_SUCCESS);
}

/**
 * Timer handler for display update
 * @param *threadid Thread ID
 */

void* displayTimerHandler(void* threadid) {
	int scale1Hz = 0;
	if (timerScaler++ >= REFRESH_RATE) { // 1s elapsed, reset
		timerScaler = 0;
		scale1Hz = 1;
	}

	if (scale1Hz) {
		if (coffeeMode == MODE_STATE) {
			switch (coffeeState) {
			case STATE_OFF:
			case STATE_HEATING:
			case STATE_INITALHEATING:
			case STATE_IDLE:
			case STATE_CLEANING:
			case STATE_ERROR:
			case STATE_WAIT_OFF:
				displayRefresh();
				break;
			default:
				break;
			}
		} else if (coffeeMode == MODE_MENU) {
			displayRefresh();
		}
	}

	switch (coffeeState) {
	case STATE_BREW:
	case STATE_BREWMANUAL:
		displayRefresh();
		break;
	default:
		break;
	}

	if (elapsedCnt < (5 * REFRESH_RATE)) // Don't let it grow too large
		elapsedCnt++;
	pthread_exit(EXIT_SUCCESS);
}

/**
 * Initializes display
 */

void displayInit(void) {
	lcd = lcdInit();
	if (lcd < 0)
		logger_error("Error: unable to init LCD (%d)\n", lcd);
	lcdClear(lcd);
	lcdHome(lcd);
	displayPrintLn(0, (char*) "CoffeePi", true);
	displayPrintLn(1, (char*) "booting...", true);
	//lcdPrintf(lcd, "    CoffeePi       booting...");

	timerScaler = 0;
	displayTimer.setDivider((1000000 / TIMER_DELAY_US) / REFRESH_RATE);

	logger(V_BASIC, "Initialized display with a refresh rate of %d Hz\n",
	REFRESH_RATE);
	/**The following block comes from void* displayThread(void* threadid)
	 * The idea is that the initialization functions get the component ready to react on external events
	 * once the threads start, events might be triggered and every component can process them
	 * This should fix the TO_DO where a descaling event was triggered during startup of the coffeethread and
	 * the displaythread did not react to it since it hadn't subscribed to the event yet
	 */
	int tmp = sqlGetConf(CFGdisplaylang);
	if (!tmp || tmp >= lang_last)
		tmp = DEFAULT_LANG;
	displaySetLang((display_lang_t) tmp);
	event_subscribe("statechange", &displayStateUpdated);
	event_subscribe("modechange", &displayModeUpdated);
	event_subscribe("pagechange", &displayPageUpdated);
	event_subscribe("terminate", &displayTerminate);
	event_subscribe("descaling", &displayDescaling);
}

/**
 * Sets the language of the display text
 * @param lang New language
 */

void displaySetLang(display_lang_t lang) {
	displayLang = lang;
}

/**
 * Refreshed the display content and outputs it
 */

void displayRefresh(void) {
	if (coffeeMode == MODE_STATE) {
		switch (coffeeState) { //coffeeGetState()
		case STATE_IDLE:
			displayPrintLogo();
			displayPrintLn(1, displayGetString(str_ready), true);
			break;

		case STATE_INITALHEATING:
			displayPrintLogo();
			displayPrintLn(1, displayGetString(str_heating), true);
			break;

		case STATE_HEATING:
			displayPrintLogo();
			displayPrintLn(1, displayGetString(str_heatingready), true);
			break;

		case STATE_BREW:
			if (elapsedCnt <= 2 * REFRESH_RATE) {
				displayPrintLogo();
				displayPrintLn(1, displayGetString(str_brewing), true);
			} else {
				displayPrintLn(0, displayGetString(str_brewing), true);
				displayPrintFlow(1);
			}
			break;

		case STATE_BREWMANUAL:
			displayPrintLn(0, displayGetString(str_brewing), true);
			displayPrintFlow(1);
			break;

		case STATE_CLEANING:
			displayPrintLogo();
			displayPrintLn(1, displayGetString(str_cleaning), true);
			break;

		case STATE_ERROR:
			displayPrintLogo();
			displayPrintLn(1, displayGetString(str_error), true);
			break;

		case STATE_WAIT_OFF:
			displayPrintLogo();
			displayPrintLn(1, displayGetString(str_waitoff), true);
			break;

		case STATE_OFF:
		default:
			displayPrintLogo();
			displayPrintTime(1);
			break;
		}
	} else if (coffeeMode == MODE_MENU) {
		switch (coffeePage) {
		case PAGE_SOFTOFF:
			displayPrintLn(0, displayGetString(str_menu), true);
			displayPrintLn(1, displayGetString(str_menu_softoff), true);
			break;
		case PAGE_KILL:
			displayPrintLn(0, displayGetString(str_menu), true);
			displayPrintLn(1, displayGetString(str_menu_kill), true);
			break;
		case PAGE_STATS:
			displayPrintLn(0, displayGetString(str_menu_stats), true);
			displayPrintStats(1);
			break;
		case PAGE_STATS2:
			displayPrintLn(0, displayGetString(str_menu_stats2), true);
			displayPrintStats2(1);
			break;
		case PAGE_DESCALING:
			displayPrintLn(0, displayGetString(str_menu_nextdesc), true);
			displayPrintNextDesc(1);
			break;
		case PAGE_TEMP:
			displayPrintLn(0, displayGetString(str_menu_temp), true);
			displayPrintTemp(1);
			break;
		case PAGE_CLEAN:
			displayPrintLn(0, displayGetString(str_menu), true);
			displayPrintLn(1, displayGetString(str_menu_clean), true);
			break;
		case PAGE_DEMO:
			displayPrintLn(0, displayGetString(str_menu), true);
			displayPrintLn(1, displayGetString(str_menu_demo), true);
			break;
		case PAGE_EXIT:
			displayPrintLn(0, displayGetString(str_menu), true);
			displayPrintLn(1, displayGetString(str_menu_exit), true);
			break;
		default:
			displayPrintLn(0, displayGetString(str_menu), true);
			displayPrintLn(1, "???", true);
			break;
		}
	}
}

/**
 * Returns the matching translation of a string
 * @param string Requested string
 * @return Translated string
 */

const char* displayGetString(display_strings_t string) {
	if (displayLang >= lang_last)
		displayLang = DEFAULT_LANG;
	return display_strings[string].text[displayLang];
}
