/*
 * coffee.cpp
 *
 *  Created on: Sep 25, 2017
 *      Author: sebastian
 */

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <stdint.h>
#include <wiringPi.h>
#include <pthread.h>
#include <unistd.h>
#include <iostream>
#include <csignal>
#include <time.h>
#include <ctime>
#include "coffee.h"
#include "hal.h"
#include "logger.h"
#include "timer.h"
#include "database.h"

coffee_status_t state;
coffee_menuPage_t page;
coffee_mode_t mode;

int sigValue;
int brewTime; //Brew time in ms
timer brewTimer(&brewTimeHandler);

uint64_t totalHeatingTime; //local copies of the corresponding database entries
uint16_t brewCounter;
bool initalHeating;
bool descaling; //flag to indicate descaling and cleaning
uint16_t descBrewcount;
time_t descRawTimestamp;

const char* PageName[] = { "SoftOff", "Kill", "Stats", "Stats2", "Descaling", "Temp", "Clean", "Demo",
		"Exit" };
const char* StateName[] = { "OFF", "HEATING", "INITHEAT", "IDLE", "BREW",
		"BREWMAN", "CLEAN", "ERROR", "WAITOFF" };

/**
 * Thread for the finite state machine
 * It represents the current state of the machine and handles signals coming from
 * the pressure control, buttons, the brew switch and the proximity sensor
 * @param threadid the ID of the thread
 */

void *coffeeThread(void *threadid) {
	logger(V_BASIC, "Initializing coffee thread...\n");

	//installing new Signal handler for coffeethread
	struct sigaction action;
	sigemptyset(&action.sa_mask);
	action.sa_flags = SA_SIGINFO;
	action.sa_sigaction = coffeeHandler;
	sigaction(SIGUSR2, &action, NULL);

	brewTimer.setDivider(4);
	brewTimer.stop();
	brewTime = 0;

	initalHeating = true;
	mode = MODE_STATE; //Unless we enter the menu we start in state mode
	page = PAGE_SOFTOFF;
	descaling = false;


	//read the database values
	if (!(totalHeatingTime = sqlGetConf(CFGHeatingTime))) {
		logger_error("coffee.cpp: Couldn't read the heating time from the database\n");
		//pthread_exit(EXIT_SUCCESS);
		exit(EXIT_FAILURE);
	}
	if (!(brewCounter = sqlGetConf(CFGbrewcounter))) {
		logger_error("coffee.cpp: Couldn't read the brew counter from the database\n");
		//pthread_exit(EXIT_SUCCESS);
		exit(EXIT_FAILURE);
	}
	if (!(descRawTimestamp = (time_t) sqlGetConf(CFGDescTimestamp))) {
		logger_error("coffee.cpp: Couldn't read the descaling time from the database\n");
		//pthread_exit(EXIT_SUCCESS);
		exit(EXIT_FAILURE);
	}
	if (!(descBrewcount = sqlGetConf(CFGDescBrewCount))) {
		logger_error("coffee.cpp: Couldn't read the descaling brewcount time from the database\n");
		//pthread_exit(EXIT_SUCCESS);
		exit(EXIT_FAILURE);
	}

	checkDescaling();

	event_subscribe("terminate", &coffeeTerminate);

	logger(V_BREW, "Determining inital state\n");
	//determine inital state
	if (halGetRelaisState(RELAIS_POWER) && halGetRelaisState(RELAIS_HEAT)
			&& !halGetRelaisState(RELAIS_PUMP)) {
		//wait for heat relais to switch
		coffeeNap(1,0);
		if (halIsHeating()) { //Heating is on
			changeState(STATE_INITALHEATING);
		} else {
			initalHeating = false;
			changeState(STATE_IDLE);
		}
	} else if (halGetRelaisState(RELAIS_PUMP)) {
		logger_error("Whoops, why is the pump running...\n");
		changeState(STATE_ERROR);
	} else {
		changeState(STATE_OFF);
	}

	logger(V_BREW, "Starting Coffee FSM\n");
	//begin FSM
	while (1) {

		/*
		 * Menue FSM
		 */
		switch (page) {
		case PAGE_SOFTOFF: //this page is only available when the machine is on
			if (SigValueEmpty() && mode == MODE_MENU)
				pause();

			switch (getSigValue(MODE_MENU)) {
			case SigInt0Rls:
				if(halIsHeating()){
					changeState(STATE_WAIT_OFF);
				}
				else {
					changeState(STATE_OFF);
				}
				leaveMenu();
				break;

			case SigInt1Psh:
				changePage(PAGE_KILL);
				break;
			}
			break;

		case PAGE_KILL: //this page is only available when the machine is on
			if (SigValueEmpty() && mode == MODE_MENU)
				pause();

			switch (getSigValue(MODE_MENU)) {
			case SigInt0Rls:
				if (halIsHeating()) {
					coffeeIncreaseHeatingTime(halgetHeatingTime());
				}
				changeState(STATE_OFF);
				leaveMenu();
				break;

			case SigInt1Psh:
				if (state == STATE_IDLE || state == STATE_HEATING) {
					changePage(PAGE_CLEAN);
				} else {
					changePage(PAGE_DEMO);
				}
				break;
			}
			break;

		case PAGE_CLEAN: //this page is only be available when the machine is hot
			if (SigValueEmpty() && mode == MODE_MENU)
				pause();

			switch (getSigValue(MODE_MENU)) {
			case SigInt0Rls:
				changeMode(MODE_STATE);
				if (!halProxSensorCovered()) {
					changeState(STATE_CLEANING);
					leaveMenu();
				} else {
					changeState(STATE_FULLTANK);
					leaveMenu();
				}
				break;

			case SigInt1Psh:
				changePage(PAGE_DEMO);
				break;
			}
			break;

		case PAGE_DEMO:
			if (SigValueEmpty() && mode == MODE_MENU)
				pause();

			switch (getSigValue(MODE_MENU)) {
			case SigInt1Psh:
				changePage(PAGE_TEMP);
				break;
			}
			break;

		case PAGE_TEMP:
			if (SigValueEmpty() && mode == MODE_MENU)
				pause();

			switch (getSigValue(MODE_MENU)) {
			case SigInt1Psh:
				changePage(PAGE_STATS);
				break;
			}
			break;
		case PAGE_STATS:
			if (SigValueEmpty() && mode == MODE_MENU)
				pause();

			switch (getSigValue(MODE_MENU)) {
			case SigInt1Psh:
				changePage(PAGE_STATS2);
				break;
			}
			break;
		case PAGE_STATS2:
			if (SigValueEmpty() && mode == MODE_MENU)
				pause();

			switch (getSigValue(MODE_MENU)) {
			case SigInt1Psh:
				changePage(PAGE_DESCALING);
				break;
			}
			break;
		case PAGE_DESCALING:
			if (SigValueEmpty() && mode == MODE_MENU)
				pause();

			switch (getSigValue(MODE_MENU)) {
			case SigInt1Psh:
				changePage(PAGE_EXIT);
				break;
			}
			break;
		case PAGE_EXIT:
			if (SigValueEmpty() && mode == MODE_MENU)
				pause();

			switch (getSigValue(MODE_MENU)) {
			case SigInt0Rls:
				leaveMenu();
				break;

			case SigInt1Psh:
				if (state == STATE_HEATING || state == STATE_ERROR
						|| state == STATE_IDLE
						|| state == STATE_INITALHEATING) {
					changePage(PAGE_SOFTOFF);
				} else {
					changePage(PAGE_DEMO);
				}
				break;
			}
			break;
		} //end switch (page)

		/*
		 * Hardware FSM
		 */
		switch (state) {
		/*
		 *
		 */
		case STATE_OFF:
			if (mode == MODE_STATE) {
				halMachineOff();
				writeBackCache();
				changePage(PAGE_DEMO);
				if (SigValueEmpty())
					pause();
			}

			switch (getSigValue(MODE_STATE)) {
			case SigInt0Rls:
			case SigPowerUp:
				//Check waterlevel in gray water tank
				//turn machine on
				halMachineOn();
				coffeeNap(1,0);
				if (halIsHeating() && !halProxSensorCovered()) { //check if System starts to heat when turned on
					changeState(STATE_INITALHEATING);
				} else if (!halIsHeating() && !halProxSensorCovered()) {
					changeState(STATE_IDLE);
				} else if (halProxSensorCovered()) {
					logger_error("Empty Tank please!\n");
					changeState(STATE_FULLTANK);
				}
				if (page != PAGE_SOFTOFF)
					changePage(PAGE_SOFTOFF); //the machine is on, the menu starts with the turning off page
				break;

			case SigInt1Psh:
				//Enter the menu
				if (page != PAGE_DEMO)
					changePage(PAGE_DEMO); //machine is off, the menu starts with the demo page
				changeMode(MODE_MENU);
				break;
			}
			break;

			/*
			 *
			 */
		case STATE_WAIT_OFF:
			if (SigValueEmpty() && mode == MODE_STATE)
				pause();
			switch (getSigValue(MODE_STATE)) {
			case SigPressOpn:
				coffeeNap(1,0); //wait so no load will be switched
				coffeeIncreaseHeatingTime(halgetHeatingTime());
				changeState(STATE_OFF);
				break;

			case SigInt0Psh:
			case SigInt1Psh:
			case SigPowerUp:
				if (halProxSensorCovered()) {
					changeState(STATE_FULLTANK);
				} else if (initalHeating) {
					changeState(STATE_INITALHEATING);
				} else {
					changeState(STATE_HEATING);
				}
				break;
			}
			break;

			/*
			 *
			 */
		case STATE_INITALHEATING:
			initalHeating = true;

			if (SigValueEmpty() && mode == MODE_STATE)
				pause();
			switch (getSigValue(MODE_STATE)) {
//			case SigInt0RlsLong:
//				//Turn machine off again
//				coffeeIncreaseHeatingTime(halgetHeatingTime());
//				changeState(STATE_OFF);
//				break;
//
//			case SigInt0Rls:
//				changeState(STATE_WAIT_OFF);
//				break;

			case SigProxCvrd:
				changeState(STATE_FULLTANK);
				break;

			case SigPressOpn:
				//Inital heating finished
				initalHeating = false;
				coffeeIncreaseHeatingTime(halgetHeatingTime());
				changeState(STATE_IDLE);
				break;

			case SigPowerDown:
				changeState(STATE_WAIT_OFF);
				break;

			case SigInt1Psh:
				changeMode(MODE_MENU);
				break;
			}
			break;

			/*
			 *
			 */
		case STATE_HEATING:
			if (SigValueEmpty() && mode == MODE_STATE)
				pause();
			switch (getSigValue(MODE_STATE)) {
//			case SigInt1RlsLong:
//				//Turn machine _immediately_ off again
//				coffeeIncreaseHeatingTime(halgetHeatingTime());
//				changeState(STATE_OFF);
//				break;
//
//			case SigInt1Rls:
//				//turn machine off when heating is finished
//				changeState(STATE_WAIT_OFF);
//			break;

			case SigPressOpn:
				coffeeIncreaseHeatingTime(halgetHeatingTime());
				changeState(STATE_IDLE);
				break;

			case SigInt0Psh:
				//start to brew a delicious coffee
				changeState(STATE_BREW);
				break;

			case SigProxCvrd:
				changeState(STATE_FULLTANK);
				break;

			case SigBrewOn:
				//someone brews manually
				changeState(STATE_BREWMANUAL);
				break;

			case SigPowerDown:
				changeState(STATE_WAIT_OFF);
				break;

			case SigInt1Psh:
				//Enter the menu
				changeMode(MODE_MENU);
				break;
			}
			break;

			/*
			 *
			 */
		case STATE_IDLE:
			if (SigValueEmpty() && mode == MODE_STATE)
				pause();
			switch (getSigValue(MODE_STATE)) {
//			case SigInt1RlsLong:
//				//turn machine _immediately_ off
//				changeState(STATE_OFF);
//				break;
//
//			case SigInt1Rls:
//				changeState(STATE_OFF);
//				break;

			case SigPressCls:
				changeState(STATE_HEATING);
				break;

			case SigInt0Psh:
				changeState(STATE_BREW);
				break;

			case SigProxCvrd:
				changeState(STATE_FULLTANK);
				break;

			case SigBrewOn:
				//someone brews manually
				changeState(STATE_BREWMANUAL);
				break;

			case SigPowerDown:
				changeState(STATE_OFF);
				break;

			case SigInt1Psh:
				//Enter the menu
				changeMode(MODE_MENU);
				break;
			}
			break;

			/*
			 *
			 */
		case STATE_BREW:
			//make sure the tank is not full
			if (halProxSensorCovered()) {
				changeState(STATE_FULLTANK);
				logger_error("coffee.cpp: Full tank detection failed..\n");
			} else {
				coffeeBrew();
				checkDescaling();
				logger(V_BREW, "Finishing brewing\n");
				if (!halProxSensorCovered()) {
					if (halIsHeating()) {
						changeState(STATE_HEATING);
					} else {
						changeState(STATE_IDLE);
					}
				} else {
					changeState(STATE_FULLTANK);
				}
			}
			break;

			/*
			 *
			 */
		case STATE_BREWMANUAL:
			if (SigValueEmpty() && mode == MODE_STATE)
				pause();
			break;

			/*
			 *
			 */
		case STATE_CLEANING: //this can only be executed once the machine is hot!
			if (!halProxSensorCovered()) {
				//execute the cleaning procedure
				coffeeClean();
				if (halIsHeating()) {
					changeState(STATE_HEATING);
				} else {
					changeState(STATE_IDLE);
				}
			} else {
				changeState(STATE_FULLTANK);
			}
			break;

			/*
			 * Full tank is detected at the beginning and the end of a brewing process, during
			 * idle times, initial heating and heating
			 */
		case STATE_FULLTANK:
			if (SigValueEmpty() && mode == MODE_STATE)
				pause();
			switch (getSigValue(MODE_STATE)) {
			case SigInt1Psh:
			case SigInt0Psh:
				if (halIsHeating() && initalHeating) {
					changeState(STATE_INITALHEATING);
				} else if (halIsHeating() && !initalHeating) {
					changeState(STATE_HEATING);
				} else {
					changeState(STATE_IDLE);
				}
				break;
			}
			break;

			/*
			 *
			 */
		case STATE_ERROR:
			if (SigValueEmpty() && mode == MODE_STATE)
				pause();
			switch (getSigValue(MODE_STATE)) {
			case SigInt1RlsLong:
			case SigInt0RlsLong:
				if (halIsHeating()) {
					coffeeIncreaseHeatingTime(halgetHeatingTime());
				}
				changeState(STATE_OFF);
				break;
			}
		}
	}
	pthread_exit(EXIT_SUCCESS);
}
/**
 * Handler for the Signal send to this thread
 * It saves the type of signal received and tracks the time between a push and release event of up to 4 signals
 * The time is stored in the HalEvent variable when a release event is received
 * @param signum
 * @param siginfo
 * @param context
 */
void coffeeHandler(int signum, siginfo_t *siginfo, void *context) {
	sigval_t sigVal = (siginfo->si_value);
	sigValue = sigVal.sival_int;
	logger(V_BREW, "coffee.cpp: CoffeeHandler called with Signal %d\n",
			sigValue);
}

/**
 * returns the Signal value from the last received Signal for the given mode and clears the variable
 * @return value sent with the last signal
 */
int getSigValue(coffee_mode_t mode) {
	int tmp = sigValue;
	if (mode == MODE_MENU) {
		switch (sigValue) {
		case SigInt0Psh:
		case SigInt0Rls:
		case SigInt0RlsLong:
		case SigInt1Psh:
		case SigInt1Rls:
		case SigInt1RlsLong:
			sigValue = 0;
			return tmp;
			break;

		default:
			break;
		}
	} else { //State Mode
		sigValue = 0;
		return tmp;
	}
	//int tmp = sigValue;
	//sigValue = 0;
	//return tmp;
	return 0;
}

bool SigValueEmpty(void) {
	if (sigValue == 0)
		return true;
	else
		return false;
}

/**
 * Changes the state of the machine to newState
 * prints the change to the logger
 * @param newState
 */
void changeState(coffee_status_t newState) {
	logger(V_BREW, "Changing state to %s\n", StateName[newState]);
	state = newState;
	event_trigger("statechange", &state, sizeof(state));
}

/*
 * Change Page to new menu page
 */
void changePage(coffee_menuPage_t newPage) {
	logger(V_BREW, "Change Page to %s\n", PageName[newPage]);
	event_trigger("pagechange", &newPage, sizeof(newPage));
	page = newPage;
}

/*
 * Changes the mode of the machine to the given mode
 */
void changeMode(coffee_mode_t newMode) {
	if (newMode == MODE_MENU)
		logger(V_BREW, "Changing to menu mode\n");
	else
		logger(V_BREW, "Changing to state mode\n");

	event_trigger("modechange", &newMode, sizeof(newMode));
	mode = newMode;
}

/*
 * leaving the menu
 *  sets the start page for the next menu call to softoff
 */
void leaveMenu() {
	//TODO leave the menu after certain time automatically
	logger(V_BREW, "Leaving the menu again...\n");
	//leave the menu again
	changeMode(MODE_STATE);
	//change page to initial page
	changePage(PAGE_SOFTOFF);
}
/**
 * Returns the current state of the FSM
 */
coffee_status_t getState(void) {
	return state;
}

/**
 * Returns the local up-to-date brewcounter
 */
uint16_t getBrewCounter(void) {
	return brewCounter;
}

/**
 * Returns the total heating time in seconds
 */
uint64_t getTotalHeatingTime(void) {
	return totalHeatingTime;
}

/**
 * Returns the value of the brewcounter when the last descaling happened
 */
uint16_t getDescBrewCounter (void) {
	return descBrewcount;
}

/**
 * Returns the raw time stamp when the last descaling happened
 */
time_t * getDescTimestamp (void) {
	return &descRawTimestamp;
}

/**
 * Counter for the brew time
 * refresh every 200ms
 */
void brewTimeHandler(void) {
	brewTime += 200;
}

/**
 * handles program termination
 */
void coffeeTerminate(event_t *event) {
	logger(V_BREW, "Coffee.cpp: Terminating\n");
	//stop brewing
	halRelaisOff(RELAIS_PUMP);
	brewTimer.stop();
	writeBackCache();
}

/**
 * coffeeNap does almost the same as sleep() but resumes to sleep after a signal got delivered
 * sec: seconds to sleep
 * nanosec: nanoseconds to sleep
 */
void coffeeNap (uint64_t sec, uint64_t nanosec){
	struct timespec sleepTime;
	clock_gettime(CLOCK_REALTIME, &sleepTime);
	sleepTime.tv_sec += sec;
	sleepTime.tv_nsec += nanosec;
	//overflow
	if(sleepTime.tv_nsec >= 1000000000) {
		sleepTime.tv_nsec -= 1000000000;
		sleepTime.tv_sec++;
	}
	int errval = 0;
	do{
		errval = clock_nanosleep(CLOCK_REALTIME, TIMER_ABSTIME, &sleepTime, NULL);
	}
	while(errval == EINTR);
	if(errval) logger_error("coffee.cpp: suspending thread failed with error %d\n", errval);
}

/**
 * Function to write back the values of the local copies
 * brewCounter and totalHeatingTime
 *
 */
void writeBackCache(void) {
	logger(V_BREW, "Writing back cache...\n");
	logger(V_BREW, "Writing back brewCounter with %d\n", brewCounter);
	if (sqlSetConf(CFGbrewcounter, brewCounter)) {
		logger_error("coffee.cpp: Couldn't write brewcounter to database");
		return;
	}
	logger(V_BREW, "Writing back total heating Time with %lld sec\n", totalHeatingTime);
	if (sqlSetConf(CFGHeatingTime, totalHeatingTime)) {
		logger_error("coffee.cpp: Couldn't write heating time to database");
		return;
	}
	logger(V_BREW, "Writing back descaling brew counter %d\n", descBrewcount);
	if (sqlSetConf(CFGDescBrewCount, descBrewcount)) {
		logger_error("coffee.cpp: Couldn't write descaling brewcount to database");
		return;
	}
	struct tm * timeinfo;
	timeinfo = localtime(&descRawTimestamp);
	logger(V_BREW, "Writing back descaling timestamp %d-%d-%d %d:%d\n", timeinfo->tm_mday, timeinfo->tm_mon, timeinfo->tm_year+1900, timeinfo->tm_hour, timeinfo->tm_min);
	if (sqlSetConf(CFGDescTimestamp, (uint64_t)descRawTimestamp)) {
		logger_error("coffee.cpp: Couldn't write descaling timestamp to database");
		return;
	}
}

/*
 * Procedure for cleaning the machine
 */
void coffeeClean(void) {
	logger(V_BREW, "Cleaning...\n");
	for (int i = 0; i < 20; i++) {
		halPumpOn();
		coffeeNap(3,0);
		halPumpOff();
		coffeeNap(15,0);
	}
	updateDescaling();
	descaling = false;
	event_trigger("descaling", &descaling, sizeof(bool));
}

/**
 * Brewing process
 */
void coffeeBrew(void) {
	coffeeIncreaseBrewCounter();
	/*
	 * Preinfusion
	 */
	logger(V_BREW, "Starting preinfusion...\n");
	halResetFlow();
	halPumpOn();
	brewTime = 0;
	brewTimer.start();
	while (halGetFlow() < AMOUNT_PREINFUSION && brewTime < TIME_PREINFUSION) {
		coffeeNap(0, 50000000);
		if (getSigValue(MODE_STATE) == SigInt0Psh){
			stopBrewing();
			return;
		}
	}
	stopBrewing();

	/*
	 * Wait for coffee to soak in infused water
	 */
	brewTimer.start();
	while (brewTime < TIME_SOAK) {
		coffeeNap(1, 100000000);
		if (getSigValue(MODE_STATE) == SigInt0Psh) {
			stopBrewing();
			return;
		}
	}
	stopBrewing();

	/*
	 * Brewing the actual espresso
	 */
	logger(V_BREW, "Starting infusion...\n");
	halPumpOn();
	brewTimer.start();
	while (brewTime < TIME_INFUSION && halGetFlow() < AMOUNT_DBLESPRESSO) {
		coffeeNap(1, 100000000);
		if (getSigValue(MODE_STATE) == SigInt0Psh){
			stopBrewing();
			break;
		}
	}
	stopBrewing();
	return;
	//TODO: I want to see the total elapsed brewing time!!
}

/*
 * Wrapper function for the end of a brewing process
 * this function stops the pump, brewtimer and resets the flow and brew time to zero
 */
void stopBrewing() {
	halPumpOff();
	brewTimer.stop();
	brewTime = 0;
	halResetFlow();
}

/**
 *
 */
void coffeeIncreaseBrewCounter(void) {
	brewCounter++;
}

/**
 *
 */
void coffeeIncreaseHeatingTime(uint64_t heatingTime) {
	totalHeatingTime += heatingTime;
}



/**
 * Checks if the descaling is necessary
 * uses descBrewcount and descTimestamp
 */
void checkDescaling(){
	int16_t dirtyEspresso = checkDirtyEspresso ();
	int16_t dirtyTime = checkDirtyTime ();

	if(dirtyEspresso <= 0) {
		logger(V_BREW, "Descaling necessary due to quantity: %d\n", dirtyEspresso);
		descaling = true;
		event_trigger("descaling", &descaling, sizeof(bool));

	}
	if(dirtyTime <= 0) {
		logger(V_BREW, "Descaling necessary due to time in days: %d\n", dirtyTime);
		descaling = true;
		event_trigger("descaling", &descaling, sizeof(bool));
	}
}

/**
 * this function returns the remaining espressi to be brewed before the descaling event is fired
 * returns a positive integer when there are cups remaining
 * and a negative when the number of cups are exceeded
 * Number of cups after a descaling is defined with DIRTY_ESPRESSO
 */
int16_t checkDirtyEspresso (void) {
	return descBrewcount + DIRTY_ESPRESSO - brewCounter;
}

/**
 * Returns the remaining days before the next descaling event is fired
 * returns a positive integer if there is time left and a negative one if the descaling time was exceeded
 */
int16_t checkDirtyTime (void) {
	time_t rawtime;
	time(&rawtime);
	double diffseconds = difftime(rawtime, descRawTimestamp);
	diffseconds /= 24*60*60; //calculate the days
	return DIRTY_TIME - diffseconds;
}

/**
 * updates the corresponding variables after a descaling process
 */
void updateDescaling(){
	descBrewcount = brewCounter;
	time_t newDesTimestamp;
	time(&newDesTimestamp);
	if(newDesTimestamp == -1){
		logger(V_BREW, "Whoops, couldn't retrieve new descaling timestamp\n");
	}
	else {
		descRawTimestamp = newDesTimestamp;
	}
}

