/*
 * Author: Pawel Jablonski
 * E-mail: pj@xirx.net
 * WWW: xirx.net
 * GIT: git.xirx.net
 *
 * License: You can use this code however you like
 * but leave information about the original author.
 * Code is free for non-commercial and commercial use.
 */

#include <stm32f0xx.h>
#include <stm32f0xx_hal.h>
#include <stm32f0xx_hal_gpio.h>
#include <stm32f0xx_hal_rcc.h>
#include <system_stm32f0xx.h>

#include "type.h"
#include "keyboard.h"
#include "led.h"
#include "lcd.h"
#include "rs232.h"
#include "rtc.h"
#include "speaker.h"
#include "fs.h"

#include "io.h"

//!< List of motherboard's virtual registers
enum IORegAddress
{
	IO_REG_KEYBOARD = 0, //!< Keyboard register
	IO_REG_LED_COMMAND = 1, //!< LED register
	IO_REG_ADDRESS_LOW = 2, //!< Lower address register
	IO_REG_ADDRESS_HIGH = 3, //!< Upper address register
	IO_REG_LCD_COLOR = 4, //!< LCD color register
	IO_REG_LCD_CHAR = 5, //!< LCD char register
	IO_REG_LCD_COMMAND = 6, //!< LCD command register
	IO_REG_RS232_RX = 7, //!< RS232 receive register
	IO_REG_RS232_TX = 8, //!< RS232 transmit register
	IO_REG_RTC_FIELD = 9, //!< Real Time Clock data register
	IO_REG_RTC_COMMAND = 10, //!< Real Time Clock command register
	IO_REG_SPEAKER_FIELD = 11, //!< Speaker data register
	IO_REG_SPEAKER_COMMAND = 12, //!< Speaker command register
	IO_REG_FS_NAME = 13, //!< File System name register
	IO_REG_FS_DATA = 14, //!< File System data register
	IO_REG_FS_COMMAND = 15 //!< File System command register
};

//!< List of LED commands
enum IOLedCommand
{
	IO_LED_RUN_OFF = 0, //!< Turn off run LED
	IO_LED_RUN_ON, //!< Turn on run LED
	IO_LED_ERROR_OFF, //!< Turn off error LED
	IO_LED_ERROR_ON //!< Turn on error LED
};

//!< List of LCD commands
enum IOLcdCommand
{
	IO_LCD_CLEAR = 0, //!< Clear the screen
	IO_LCD_REFRESH, //!< Refresh the screen using data from the framebuffer
	IO_LCD_SCROLL //!< Scroll up one line up the screen
};

//!< List of Real Time Clock fields
enum IORtcField
{
	IO_RTC_YEAR = 0, //!< Year
	IO_RTC_MONTH, //!< Month
	IO_RTC_DAY, //!< Day
	IO_RTC_HOUR, //!< Hour
	IO_RTC_MINUTE, //!< Minute
	IO_RTC_SECOND //!< Second
};

//!< List of Real Time Clock commands
enum IORtcCommand
{
	IO_RTC_READ = 0, //!< Read current date and time
	IO_RTC_WRITE //!< Set current date and time
};

//!< List of Speaker fields
enum IOSpeakerField
{
	IO_SPEAKER_NOTE = 0, //!< Note number
	IO_SPEAKER_TIME, //!< Time to play
	IO_SPEAKER_FILL, //!< Percentage fill of the square wave
	IO_SPEAKER_VOLUME //!< Volume
};

//!< List of Speaker commands
enum IOSpeakerCommand
{
	IO_SPEAKER_CLEAR = 0, //!< Stop playing and clear the buffer
	IO_SPEAKER_ADD_NOTE, //!< Add note to the buffer and play it
};

//!< List of File System commands
enum IOFSCommand
{
	IO_FS_OPEN_APP = 0, //!< Open an application
	IO_FS_OPEN_FILE, //!< Open a file
	IO_FS_LIST_APP, //!< List all applications
	IO_FS_LIST_FILE, //!< List all files
	IO_FS_SIZE, //!< Get size of the file
	IO_FS_READ_BLOCK //!< Read data from the block
};

//!< This struct cointains a memory adddress
struct IOAddress
{
	unsigned char low; //!< Lower byte of the address
	unsigned char high; //!< Upper byte of the address
};

static unsigned char regSelected = 0x00; //!< Selected virtual register number

//!< This struct contains motherboard's virtual registers buffer
struct
{
	unsigned char keyboard; //!< Keyboard buffer
	struct IOAddress address; //!< Address buffer
	unsigned char lcdForegroundColor; //!< LCD foreground color buffer
	unsigned char lcdBackgroundColor; //!< LCD background color buffer
	unsigned char lcdChar; //!< LCD char buffer
	unsigned char rs232Rx; //!< RS232 receive buffer
	unsigned char rs232Tx; //!< RS232 transmit bufffer
	unsigned char rs232TxBufferFree; //!< RS232 transmit free space in buffer counter
	struct RTCDateTime rtcField; //!< Real Time Clock buffer
	struct SpeakerNote speakerField; //!< Speaker buffer
	unsigned char speakerBufferFree; //!< Speaker free space in buffer counter
	struct FSName fsName; //!< File System name buffer
	struct FSSize fsSize; //!< File System size buffer
	struct FSBlock fsBlock; //!< File System data block buffer
} static reg;


//! First stage of init. It set up all IO pins. It must be executed before all devices init.
void ioPreInit(void)
{
	GPIO_InitTypeDef gpioInit = {0};
	
	__HAL_RCC_GPIOB_CLK_ENABLE();
	
	gpioInit.Pin = (GPIO_PIN_8 | GPIO_PIN_9 | GPIO_PIN_10 | GPIO_PIN_11 | GPIO_PIN_12 | GPIO_PIN_13 | GPIO_PIN_14);
	gpioInit.Mode = GPIO_MODE_INPUT;
	gpioInit.Pull = GPIO_PULLDOWN;
	gpioInit.Speed = GPIO_SPEED_FREQ_HIGH;
	
	HAL_GPIO_Init(GPIOB, &gpioInit);
	
	gpioInit.Pin = (GPIO_PIN_0 | GPIO_PIN_1 | GPIO_PIN_2 | GPIO_PIN_3 | GPIO_PIN_4 | GPIO_PIN_5 | GPIO_PIN_6 | GPIO_PIN_7);
	gpioInit.Mode = GPIO_MODE_OUTPUT_PP;
	gpioInit.Pull = GPIO_NOPULL;
	gpioInit.Speed = GPIO_SPEED_FREQ_HIGH;
	
	HAL_GPIO_Init(GPIOB, &gpioInit);
	
	HAL_GPIO_WritePin(GPIOB, GPIO_PIN_0, GPIO_PIN_RESET);
	HAL_GPIO_WritePin(GPIOB, GPIO_PIN_1, GPIO_PIN_RESET);
	HAL_GPIO_WritePin(GPIOB, GPIO_PIN_2, GPIO_PIN_RESET);
	HAL_GPIO_WritePin(GPIOB, GPIO_PIN_3, GPIO_PIN_RESET);
	HAL_GPIO_WritePin(GPIOB, GPIO_PIN_4, GPIO_PIN_RESET);
	HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_RESET);
	HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET);
	HAL_GPIO_WritePin(GPIOB, GPIO_PIN_7, GPIO_PIN_RESET);
}

//! Second stage of init. It set up the IRQ used in IO communication. I can only be executed when all devices are inited.
void ioInit(void)
{
	GPIO_InitTypeDef gpioInit = {0};
	
	gpioInit.Pin = GPIO_PIN_15;
	gpioInit.Mode = GPIO_MODE_IT_RISING;
	gpioInit.Pull = GPIO_PULLDOWN;
	gpioInit.Speed = GPIO_SPEED_FREQ_HIGH;
	
	HAL_GPIO_Init(GPIOB, &gpioInit);

	HAL_NVIC_SetPriority(EXTI4_15_IRQn, 4, 0);
	HAL_NVIC_EnableIRQ(EXTI4_15_IRQn);
}

//! Process the IO clock fire event. Toggle the clock pin.
void ioFireClock(void)
{
	HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_4);
}

//! Process the IO peripherial fire event. Check if the keyboard or the rs232 have a new data in the buffers.
void ioFirePeripherial(void)
{
	if(keyboardIsData())
	{
		HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_SET);
	}
	else
	{
		HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_RESET);
	}
	
	if(rs232RxIsData())
	{
		HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_SET);
	}
	else
	{
		HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET);
	}
}

//! Process the IRQ event for the IO. It processes a new incoming request from the XiPU.
void EXTI4_15_IRQHandler(void)
{
	if(__HAL_GPIO_EXTI_GET_IT(GPIO_PIN_15) != RESET) 
	{
		unsigned char value = ((GPIOB->IDR >> 8) & 0xff);
		
		unsigned char half = (value & (1 << 4));
		unsigned char mode = (value & (1 << 5));
		unsigned char rw = (value & (1 << 6));

		if(!mode)
		{
			// Select register to work
			regSelected = (value & IO_REG_SELECT_MASK);
		}
		else
		{
			if(!rw)
			{
				unsigned char dataOut = 0x00;

				// Read register value
				switch(regSelected)
				{
					case IO_REG_KEYBOARD :
						{
							if(!half)
							{
								reg.keyboard = keyboardGetKey();
								
								if(!keyboardIsData())
								{
									HAL_GPIO_WritePin(GPIOB, GPIO_PIN_5, GPIO_PIN_RESET);
								}
							}

							dataOut = reg.keyboard;
						}
						break;

					case IO_REG_RS232_RX :
						{
							if(!half)
							{
								reg.rs232Rx = rs232Receive();
								
								if(!rs232RxIsData())
								{
									HAL_GPIO_WritePin(GPIOB, GPIO_PIN_6, GPIO_PIN_RESET);
								}
							}

							dataOut = reg.rs232Rx;
						}
						break;

					case IO_REG_RTC_FIELD :
						switch(reg.address.low)
						{
							case IO_RTC_YEAR :
								dataOut = reg.rtcField.year;
								break;

							case IO_RTC_MONTH :
								dataOut = reg.rtcField.month;
								break;

							case IO_RTC_DAY :
								dataOut = reg.rtcField.day;
								break;

							case IO_RTC_HOUR :
								dataOut = reg.rtcField.hour;
								break;

							case IO_RTC_MINUTE :
								dataOut = reg.rtcField.minute;
								break;

							case IO_RTC_SECOND :
								dataOut = reg.rtcField.second;
								break;

							default :
								dataOut = 0;
								break;
						}
						break;

					case IO_REG_SPEAKER_FIELD :
						{
							if(!half)
							{
								reg.speakerBufferFree = speakerGetBufferFree();
							}

							dataOut = reg.speakerBufferFree;
						}
						break;

					case IO_REG_FS_DATA :
						{
							dataOut = reg.fsBlock.data[reg.address.low];

							if(half)
							{
								reg.address.low++;
							}
						}
						break;

					case IO_REG_FS_COMMAND :
						switch(reg.address.low)
						{
							case IO_FS_OPEN_APP :
								{
									reg.fsName.data[FS_NAME_MAX_LENGTH] = 0;

									if(fsOpen("app", &(reg.fsName), &(reg.fsSize)))
									{
										dataOut = 1;
									}
									else
									{
										dataOut = 0;
									}
								}
								break;

							case IO_FS_OPEN_FILE :
								{
									reg.fsName.data[FS_NAME_MAX_LENGTH] = 0;

									if(fsOpen("file", &(reg.fsName), &(reg.fsSize)))
									{
										dataOut = 1;
									}
									else
									{
										dataOut = 0;
									}
								}
								break;

							case IO_FS_LIST_APP :
								{
									if(fsList("app", &(reg.fsSize)))
									{
										dataOut = 1;
									}
									else
									{
										dataOut = 0;
									}
								}
								break;

							case IO_FS_LIST_FILE :
								{
									if(fsList("file", &(reg.fsSize)))
									{
										dataOut = 1;
									}
									else
									{
										dataOut = 0;
									}
								}
								break;

							case IO_FS_SIZE :
								switch(reg.address.high)
								{
									case 0 :
										dataOut = reg.fsSize.low;
										break;

									case 1 :
										dataOut = reg.fsSize.high;
										break;

									default:
										dataOut = 0;
										break;
								}
								break;

							default :
								dataOut = 0;
								break;
						}
						break;

					default :
						dataOut = 0;
						break;
				}

				if(half)
				{
					dataOut >>= 4;
				}

				// Register value to out
				GPIOB->ODR = ((GPIOB->ODR & IO_IN_CONTROL_MASK) | (dataOut & IO_IN_DATA_MASK));
			}
			else
			{
				unsigned char dataInRaw = (value & IO_IN_DATA_MASK);
				unsigned char dataIn = dataInRaw;
				unsigned char mask = IO_HALF_UP_DATA_MASK;

				if(half)
				{
					dataIn <<= IO_HALF_DATA_OFFSET;
					mask >>= IO_HALF_DATA_OFFSET;
				}

				// Write register value
				switch(regSelected)
				{
					case IO_REG_LED_COMMAND :
						switch(dataInRaw)
						{
							case IO_LED_RUN_OFF :
								ledSetRun(FALSE);
								break;
							case IO_LED_RUN_ON :
								ledSetRun(TRUE);
								break;
							case IO_LED_ERROR_OFF :
								ledSetError(FALSE);
								break;
							case IO_LED_ERROR_ON :
								ledSetError(TRUE);
								break;
						}
						break;

					case IO_REG_ADDRESS_LOW :
						reg.address.low = ((reg.address.low & mask) | dataIn);
						break;

					case IO_REG_ADDRESS_HIGH :
						reg.address.high = ((reg.address.high & mask) | dataIn);
						break;

					case IO_REG_LCD_COLOR :
						if(!half)
						{
							reg.lcdForegroundColor = dataInRaw;
						}
						else
						{
							reg.lcdBackgroundColor = dataInRaw;
						}
						break;

					case IO_REG_LCD_CHAR :
						reg.lcdChar = ((reg.lcdChar & mask) | dataIn);
						if(half)
						{
							lcdSetChar(reg.lcdChar, reg.address.low, reg.address.high, reg.lcdForegroundColor, reg.lcdBackgroundColor);
						}
						break;

					case IO_REG_LCD_COMMAND :
						switch(dataIn)
						{
							case IO_LCD_CLEAR :
								lcdClear();
								lcdRefresh();
								break;
							case IO_LCD_REFRESH :
								lcdRefresh();
								break;
							case IO_LCD_SCROLL :
								lcdScroll();
								break;
						}
						break;

					case IO_REG_RS232_TX :
						reg.rs232Tx = ((reg.rs232Tx & mask) | dataIn);
						if(half)
						{
							rs232Send(reg.rs232Tx);
						}
						break;

					case IO_REG_RTC_FIELD :
						switch(reg.address.low)
						{
							case IO_RTC_YEAR :
								reg.rtcField.year = ((reg.rtcField.year & mask) | dataIn);
								break;

							case IO_RTC_MONTH :
								reg.rtcField.month = ((reg.rtcField.month & mask) | dataIn);
								break;

							case IO_RTC_DAY :
								reg.rtcField.day = ((reg.rtcField.day & mask) | dataIn);
								break;

							case IO_RTC_HOUR :
								reg.rtcField.hour = ((reg.rtcField.hour & mask) | dataIn);
								break;

							case IO_RTC_MINUTE :
								reg.rtcField.minute = ((reg.rtcField.minute & mask) | dataIn);
								break;

							case IO_RTC_SECOND :
								reg.rtcField.second = ((reg.rtcField.second & mask) | dataIn);
								break;
						}
						break;

					case IO_REG_RTC_COMMAND :
						switch(dataInRaw)
						{
							case IO_RTC_READ :
								reg.rtcField = rtcGetDateTime();
								break;

							case IO_RTC_WRITE :
								rtcSetDateTime(reg.rtcField);
								break;
						}
						break;

					case IO_REG_SPEAKER_FIELD :
						switch(reg.address.low)
						{
							case IO_SPEAKER_NOTE :
								reg.speakerField.note = ((reg.speakerField.note & mask) | dataIn);
								break;

							case IO_SPEAKER_TIME :
								reg.speakerField.time = ((reg.speakerField.time & mask) | dataIn);
								break;

							case IO_SPEAKER_FILL :
								reg.speakerField.fill = dataInRaw;
								break;

							case IO_SPEAKER_VOLUME :
								reg.speakerField.volume = dataInRaw;
								break;
						}
						break;

					case IO_REG_SPEAKER_COMMAND :
						switch(dataInRaw)
						{
							case IO_SPEAKER_CLEAR :
								speakerClear();
								break;

							case IO_SPEAKER_ADD_NOTE :
								speakerAddNote(reg.speakerField);
								break;
						}
						break;

					case IO_REG_FS_NAME :
						if(reg.address.low < FS_NAME_MAX_LENGTH)
						{
							reg.fsName.data[reg.address.low] = ((reg.fsName.data[reg.address.low] & mask) | dataIn);
						}
						break;

					case IO_REG_FS_COMMAND :
						switch(dataInRaw)
						{
							case IO_FS_READ_BLOCK :
								fsReadBlock(&(reg.fsBlock), reg.address.low);
								break;
						}
						break;
				}
			}
		}
		
		__HAL_GPIO_EXTI_CLEAR_IT(GPIO_PIN_15);
		
		// Operation is completed
		HAL_GPIO_TogglePin(GPIOB, GPIO_PIN_7);
	}
}