/*
 * OLED.cpp
 *
 *  Created on: 29Aug.,2017
 *      Author: Ben V. Brown
 */

#include <string.h>
#include <OLED.hpp>
#include <stdlib.h>
#include "Translation.h"
#include "cmsis_os.h"
#include "../../configuration.h"

const uint8_t *OLED::currentFont;  // Pointer to the current font used for
// rendering to the buffer
uint8_t *OLED::firstStripPtr;      // Pointers to the strips to allow for buffer
// having extra content
uint8_t *OLED::secondStripPtr;     // Pointers to the strips
bool OLED::inLeftHandedMode;   // Whether the screen is in left or not (used for
// offsets in GRAM)
OLED::DisplayState OLED::displayState;
uint8_t OLED::fontWidth, OLED::fontHeight;
int16_t OLED::cursor_x, OLED::cursor_y;
uint8_t OLED::displayOffset;
uint8_t OLED::screenBuffer[16 + (OLED_WIDTH * 2) + 10];  // The data buffer
uint8_t OLED::secondFrameBuffer[OLED_WIDTH * 2];

/*Setup params for the OLED screen*/
/*http://www.displayfuture.com/Display/datasheet/controller/SSD1307.pdf*/
/*All commands are prefixed with 0x80*/
/*Data packets are prefixed with 0x40*/
FRToSI2C::I2C_REG OLED_Setup_Array[] = {
/**/
{ 0x80, 0xAE, 0 }, /*Display off*/
{ 0x80, 0xD5, 0 }, /*Set display clock divide ratio / osc freq*/
{ 0x80, 0x52, 0 }, /*Divide ratios*/
{ 0x80, 0xA8, 0 }, /*Set Multiplex Ratio*/
{ 0x80, 0x0F, 0 }, /*16 == max brightness,39==dimmest*/
{ 0x80, 0xC0, 0 }, /*Set COM Scan direction*/
{ 0x80, 0xD3, 0 }, /*Set vertical Display offset*/
{ 0x80, 0x00, 0 }, /*0 Offset*/
{ 0x80, 0x40, 0 }, /*Set Display start line to 0*/
{ 0x80, 0xA0, 0 }, /*Set Segment remap to normal*/
{ 0x80, 0x8D, 0 }, /*Charge Pump*/
{ 0x80, 0x14, 0 }, /*Charge Pump settings*/
{ 0x80, 0xDA, 0 }, /*Set VCOM Pins hardware config*/
{ 0x80, 0x02, 0 }, /*Combination 2*/
{ 0x80, 0x81, 0 }, /*Contrast*/
{ 0x80, 0x33, 0 }, /*^51*/
{ 0x80, 0xD9, 0 }, /*Set pre-charge period*/
{ 0x80, 0xF1, 0 }, /*Pre charge period*/
{ 0x80, 0xDB, 0 }, /*Adjust VCOMH regulator ouput*/
{ 0x80, 0x30, 0 }, /*VCOM level*/
{ 0x80, 0xA4, 0 }, /*Enable the display GDDR*/
{ 0x80, 0XA6, 0 }, /*Normal display*/
{ 0x80, 0x20, 0 }, /*Memory Mode*/
{ 0x80, 0x00, 0 }, /*Wrap memory*/
{ 0x80, 0xAF, 0 }, /*Display on*/
};
// Setup based on the SSD1307 and modified for the SSD1306

const uint8_t REFRESH_COMMANDS[17] = { 0x80, 0xAF, 0x80, 0x21, 0x80, 0x20, 0x80, 0x7F, 0x80, 0xC0, 0x80, 0x22, 0x80, 0x00, 0x80, 0x01, 0x40 };

/*
 * Animation timing function that follows a bezier curve.
 * @param t A given percentage value [0..<100]
 * Returns a new percentage value with ease in and ease out.
 * Original floating point formula: t * t * (3.0f - 2.0f * t);
 */
static uint8_t easeInOutTiming(uint8_t t) {
	return t * t * (300 - 2 * t) / 10000;
}

/*
 * Returns the value between a and b, using a percentage value t.
 * @param a The value associated with 0%
 * @param b The value associated with 100%
 * @param t The percentage [0..<100]
 */
static uint8_t lerp(uint8_t a, uint8_t b, uint8_t t) {
	return a + t * (b - a) / 100;
}

void OLED::initialize() {
	cursor_x = cursor_y = 0;
	currentFont = USER_FONT_12;
	fontWidth = 12;
	inLeftHandedMode = false;
	firstStripPtr = &screenBuffer[FRAMEBUFFER_START];
	secondStripPtr = &screenBuffer[FRAMEBUFFER_START + OLED_WIDTH];
	fontHeight = 16;
	displayOffset = 0;
	memcpy(&screenBuffer[0], &REFRESH_COMMANDS[0], sizeof(REFRESH_COMMANDS));

	// Set the display to be ON once the settings block is sent and send the
	// initialisation data to the OLED.

	setDisplayState(DisplayState::ON);
	FRToSI2C::writeRegistersBulk(DEVICEADDR_OLED, OLED_Setup_Array, sizeof(OLED_Setup_Array) / sizeof(OLED_Setup_Array[0]));

}

void OLED::setFramebuffer(uint8_t *buffer) {
	if (buffer == NULL) {
		firstStripPtr = &screenBuffer[FRAMEBUFFER_START];
		secondStripPtr = &screenBuffer[FRAMEBUFFER_START + OLED_WIDTH];
		return;
	}

	firstStripPtr = &buffer[0];
	secondStripPtr = &buffer[OLED_WIDTH];
}

/*
 * Prints a char to the screen.
 * UTF font handling is done using the two input chars.
 * Precursor is the command char that is used to select the table.
 */
void OLED::drawChar(char c) {
	if (c == '\x01' && cursor_y == 0) { // 0x01 is used as new line char
		cursor_x = 0;
		cursor_y = 8;
		return;
	} else if (c == 0) {
		return;
	}
	uint16_t index = c - 2; //First index is \x02
	uint8_t *charPointer;
	charPointer = ((uint8_t*) currentFont) + ((fontWidth * (fontHeight / 8)) * index);
	drawArea(cursor_x, cursor_y, fontWidth, fontHeight, charPointer);
	cursor_x += fontWidth;
}

/*
 * Draws a one pixel wide scrolling indicator. y is the upper vertical position
 * of the indicator in pixels (0..<16).
 */
void OLED::drawScrollIndicator(uint8_t y, uint8_t height) {
	union u_type {
		uint16_t whole;
		uint8_t strips[2];
	} column;

	column.whole = (1 << height) - 1;
	column.whole <<= y;

	// Draw a one pixel wide bar to the left with a single pixel as
	// the scroll indicator.
	fillArea(OLED_WIDTH - 1, 0, 1, 8, column.strips[0]);
	fillArea(OLED_WIDTH - 1, 8, 1, 8, column.strips[1]);
}

/**
 * Plays a transition animation between two framebuffers.
 * @param forwardNavigation Direction of the navigation animation.
 *
 * If forward is true, this displays a forward navigation to the second framebuffer contents.
 * Otherwise a rewinding navigation animation is shown to the second framebuffer contents.
 */
void OLED::transitionSecondaryFramebuffer(bool forwardNavigation) {
	uint8_t *firstBackStripPtr = &secondFrameBuffer[0];
	uint8_t *secondBackStripPtr = &secondFrameBuffer[OLED_WIDTH];

	uint32_t totalDuration = 50; // 500ms
	uint32_t duration = 0;
	uint32_t start = xTaskGetTickCount();
	uint8_t offset = 0;

	while (duration <= totalDuration) {
		duration = xTaskGetTickCount() - start;
		uint8_t progress = duration * 1000 / totalDuration;
		progress = easeInOutTiming(progress);
		progress = lerp(0, OLED_WIDTH, progress);
		if (progress > OLED_WIDTH) {
			progress = OLED_WIDTH;
		}

		// When forward, current contents move to the left out.
		// Otherwise the contents move to the right out.
		uint8_t oldStart = forwardNavigation ? 0 : progress;
		uint8_t oldPrevious = forwardNavigation ? progress - offset : offset;

		// Content from the second framebuffer moves in from the right (forward)
		// or from the left (not forward).
		uint8_t newStart = forwardNavigation ? OLED_WIDTH - progress : 0;
		uint8_t newEnd = forwardNavigation ? 0 : OLED_WIDTH - progress;

		offset = progress;

		memmove(&firstStripPtr[oldStart], &firstStripPtr[oldPrevious],
		OLED_WIDTH - progress);
		memmove(&secondStripPtr[oldStart], &secondStripPtr[oldPrevious],
		OLED_WIDTH - progress);

		memmove(&firstStripPtr[newStart], &firstBackStripPtr[newEnd], progress);
		memmove(&secondStripPtr[newStart], &secondBackStripPtr[newEnd], progress);

		refresh();
		osDelay(40);
	}
}

void OLED::useSecondaryFramebuffer(bool useSecondary) {
	if (useSecondary) {
		setFramebuffer(secondFrameBuffer);
	} else {
		setFramebuffer(NULL);
	}
}

void OLED::setRotation(bool leftHanded) {
#ifdef OLED_FLIP
	leftHanded = !leftHanded;
#endif
	if (inLeftHandedMode == leftHanded) {
		return;
	}

	// send command struct again with changes
	if (leftHanded) {
		OLED_Setup_Array[5].val = 0xC8;  // c1?
		OLED_Setup_Array[9].val = 0xA1;
	} else {
		OLED_Setup_Array[5].val = 0xC0;
		OLED_Setup_Array[9].val = 0xA0;
	}
	FRToSI2C::writeRegistersBulk(DEVICEADDR_OLED, OLED_Setup_Array, sizeof(OLED_Setup_Array) / sizeof(OLED_Setup_Array[0]));

	inLeftHandedMode = leftHanded;

	screenBuffer[5] = inLeftHandedMode ? 0 : 32; // display is shifted by 32 in left handed
												 // mode as driver ram is 128 wide
	screenBuffer[7] = inLeftHandedMode ? 95 : 0x7F; // End address of the ram segment we are writing to (96 wide)
	screenBuffer[9] = inLeftHandedMode ? 0xC8 : 0xC0;
}

// print a string to the current cursor location
void OLED::print(const char *str) {
	while (str[0]) {
		drawChar(str[0]);
		str++;
	}
}

void OLED::setFont(uint8_t fontNumber) {
	if (fontNumber == 1) {
		// small font
		currentFont = USER_FONT_6x8;
		fontHeight = 8;
		fontWidth = 6;
	} else if (fontNumber == 2) {
		currentFont = ExtraFontChars;
		fontHeight = 16;
		fontWidth = 12;
	} else {
		currentFont = USER_FONT_12;
		fontHeight = 16;
		fontWidth = 12;
	}
}
uint8_t OLED::getFont() {
	if (currentFont == USER_FONT_6x8)
		return 1;
	else if (currentFont == ExtraFontChars)
		return 2;
	else
		return 0;
}
inline void stripLeaderZeros(char *buffer, uint8_t places) {
	//Removing the leading zero's by swapping them to SymbolSpace
	// Stop 1 short so that we dont blank entire number if its zero
	for (int i = 0; i < (places - 1); i++) {
		if (buffer[i] == 2) {
			buffer[i] = SymbolSpace[0];
		} else {
			return;
		}
	}
}
// maximum places is 5
void OLED::printNumber(uint16_t number, uint8_t places, bool noLeaderZeros) {
	char buffer[7] = { 0 };

	if (places >= 5) {
		buffer[5] = 2 + number % 10;
		number /= 10;
	}
	if (places > 4) {
		buffer[4] = 2 + number % 10;
		number /= 10;
	}

	if (places > 3) {
		buffer[3] = 2 + number % 10;
		number /= 10;
	}

	if (places > 2) {
		buffer[2] = 2 + number % 10;
		number /= 10;
	}

	if (places > 1) {
		buffer[1] = 2 + number % 10;
		number /= 10;
	}

	buffer[0] = 2 + number % 10;
	if (noLeaderZeros)
		stripLeaderZeros(buffer, places);
	print(buffer);
}

void OLED::debugNumber(int32_t val) {
	if (abs(val) > 99999) {
		OLED::print(SymbolSpace); // out of bounds
		return;
	}
	if (val >= 0) {
		OLED::print(SymbolSpace);
		OLED::printNumber(val, 5);
	} else {
		OLED::print(SymbolMinus);
		OLED::printNumber(-val, 5);
	}
}

void OLED::drawSymbol(uint8_t symbolID) {
	// draw a symbol to the current cursor location
	setFont(2);
	drawChar(symbolID + 2);
	setFont(0);
}

// Draw an area, but y must be aligned on 0/8 offset
void OLED::drawArea(int16_t x, int8_t y, uint8_t wide, uint8_t height, const uint8_t *ptr) {
	// Splat this from x->x+wide in two strides
	if (x <= -wide)
		return;  // cutoffleft
	if (x > 96)
		return;      // cutoff right

	uint8_t visibleStart = 0;
	uint8_t visibleEnd = wide;

	// trimming to draw partials
	if (x < 0) {
		visibleStart -= x;  // subtract negative value == add absolute value
	}
	if (x + wide > 96) {
		visibleEnd = 96 - x;
	}

	if (y == 0) {
		// Splat first line of data
		for (uint8_t xx = visibleStart; xx < visibleEnd; xx++) {
			firstStripPtr[xx + x] = ptr[xx];
		}
	}
	if (y == 8 || height == 16) {
		// Splat the second line
		for (uint8_t xx = visibleStart; xx < visibleEnd; xx++) {
			secondStripPtr[x + xx] = ptr[xx + (height == 16 ? wide : 0)];
		}
	}
}

// Draw an area, but y must be aligned on 0/8 offset
// For data which has octets swapped in a 16-bit word.
void OLED::drawAreaSwapped(int16_t x, int8_t y, uint8_t wide, uint8_t height, const uint8_t *ptr) {
	// Splat this from x->x+wide in two strides
	if (x <= -wide)
		return;  // cutoffleft
	if (x > 96)
		return;      // cutoff right

	uint8_t visibleStart = 0;
	uint8_t visibleEnd = wide;

	// trimming to draw partials
	if (x < 0) {
		visibleStart -= x;  // subtract negative value == add absolute value
	}
	if (x + wide > 96) {
		visibleEnd = 96 - x;
	}

	if (y == 0) {
		// Splat first line of data
		for (uint8_t xx = visibleStart; xx < visibleEnd; xx += 2) {
			firstStripPtr[xx + x] = ptr[xx + 1];
			firstStripPtr[xx + x + 1] = ptr[xx];
		}
	}
	if (y == 8 || height == 16) {
		// Splat the second line
		for (uint8_t xx = visibleStart; xx < visibleEnd; xx += 2) {
			secondStripPtr[x + xx] = ptr[xx + 1 + (height == 16 ? wide : 0)];
			secondStripPtr[x + xx + 1] = ptr[xx + (height == 16 ? wide : 0)];
		}
	}
}

void OLED::fillArea(int16_t x, int8_t y, uint8_t wide, uint8_t height, const uint8_t value) {
	// Splat this from x->x+wide in two strides
	if (x <= -wide)
		return;  // cutoffleft
	if (x > 96)
		return;      // cutoff right

	uint8_t visibleStart = 0;
	uint8_t visibleEnd = wide;

	// trimming to draw partials
	if (x < 0) {
		visibleStart -= x;  // subtract negative value == add absolute value
	}
	if (x + wide > 96) {
		visibleEnd = 96 - x;
	}

	if (y == 0) {
		// Splat first line of data
		for (uint8_t xx = visibleStart; xx < visibleEnd; xx++) {
			firstStripPtr[xx + x] = value;
		}
	}
	if (y == 8 || height == 16) {
		// Splat the second line
		for (uint8_t xx = visibleStart; xx < visibleEnd; xx++) {
			secondStripPtr[x + xx] = value;
		}
	}
}

void OLED::drawFilledRect(uint8_t x0, uint8_t y0, uint8_t x1, uint8_t y1, bool clear) {
	// Draw this in 3 sections
	// This is basically a N wide version of vertical line

	// Step 1 : Draw in the top few pixels that are not /8 aligned
	// LSB is at the top of the screen
	uint8_t mask = 0xFF;
	if (y0) {
		mask = mask << (y0 % 8);
		for (uint8_t col = x0; col < x1; col++)
			if (clear)
				firstStripPtr[(y0 / 8) * 96 + col] &= ~mask;
			else
				firstStripPtr[(y0 / 8) * 96 + col] |= mask;
	}
	// Next loop down the line the total number of solids
	if (y0 / 8 != y1 / 8)
		for (uint8_t col = x0; col < x1; col++)
			for (uint8_t r = (y0 / 8); r < (y1 / 8); r++) {
				// This gives us the row index r
				if (clear)
					firstStripPtr[(r * 96) + col] = 0;
				else
					firstStripPtr[(r * 96) + col] = 0xFF;
			}

	// Finally draw the tail
	mask = ~(mask << (y1 % 8));
	for (uint8_t col = x0; col < x1; col++)
		if (clear)
			firstStripPtr[(y1 / 8) * 96 + col] &= ~mask;
		else
			firstStripPtr[(y1 / 8) * 96 + col] |= mask;
}

void OLED::drawHeatSymbol(uint8_t state) {
	// Draw symbol 14
	// Then draw over it, the bottom 5 pixels always stay. 8 pixels above that are
	// the levels masks the symbol nicely
	state /= 31;  // 0-> 8 range
	// Then we want to draw down (16-(5+state)
	uint8_t cursor_x_temp = cursor_x;
	drawSymbol(14);
	drawFilledRect(cursor_x_temp, 0, cursor_x_temp + 12, 2 + (8 - state), true);
}
