
/*
	TIM_SSD1306_I2C.cpp

	Minimal SSD1306 driver for 128x64 OLED using I2C.
	Arduino/ESP8266 version.

	- Full-screen buffer in RAM.
	- Rectangle-based dirty tracking (single bounding rectangle).
	- Vertical-byte font/bitmap format (bit 0 = top pixel).
*/

#include "TIM_SSD1306_I2C.h"

/*  Internal constants  */
#define CONTRAST_CONTROL_REG  0x81

/*  Global screen buffer  */
static uint8_t SSD1306_Buffer[SSD1306_BUFFER_SIZE];

/*  Dirty rectangle tracking  */
static bool     dirty = false;
static uint8_t  dirtyXMin = 0;
static uint8_t  dirtyYMin = 0;
static uint8_t  dirtyXMax = 0;
static uint8_t  dirtyYMax = 0;

/*  Font header helpers  */
#define FONT_WIDTH(font)      (pgm_read_byte(&(font[1])))
#define FONT_HEIGHT(font)     (pgm_read_byte(&(font[2])))
#define FIRST_ASCII(font)     (pgm_read_byte(&(font[3])))

/*  Forward declarations (internal)  */
static void ssd1306_WriteCommand(uint8_t cmd);
static void ssd1306_WriteData(uint8_t data);
static void ssd1306_MarkDirty(uint8_t x, uint8_t y, uint8_t w, uint8_t h);
static uint16_t getFontOffset(const uint8_t* font, char c);

/*-------------------------------------------------------------
	Low-level I2C helpers
-------------------------------------------------------------*/
static void ssd1306_WriteCommand(uint8_t cmd) {
	Wire.beginTransmission(SSD1306_I2C_ADDR);
	Wire.write(0x00);     /* Control byte: Co=0, D/C#=0 (command) */
	Wire.write(cmd);
	Wire.endTransmission();
}

static void ssd1306_WriteData(uint8_t data) {
	Wire.beginTransmission(SSD1306_I2C_ADDR);
	Wire.write(0x40);     /* Control byte: Co=0, D/C#=1 (data) */
	Wire.write(data);
	Wire.endTransmission();
}

/*-------------------------------------------------------------
	Dirty rectangle tracking
-------------------------------------------------------------*/
static void ssd1306_MarkDirty(uint8_t x, uint8_t y, uint8_t w, uint8_t h) {
	if (!dirty) {
		dirty = true;
		dirtyXMin = x;
		dirtyYMin = y;
		dirtyXMax = x + w - 1;
		dirtyYMax = y + h - 1;
	}
	else {
		if (x < dirtyXMin) dirtyXMin = x;
		if (y < dirtyYMin) dirtyYMin = y;
		uint8_t x2 = x + w - 1;
		uint8_t y2 = y + h - 1;
		if (x2 > dirtyXMax) dirtyXMax = x2;
		if (y2 > dirtyYMax) dirtyYMax = y2;
	}

	if (dirtyXMax >= SSD1306_WIDTH)  dirtyXMax = SSD1306_WIDTH - 1;
	if (dirtyYMax >= SSD1306_HEIGHT) dirtyYMax = SSD1306_HEIGHT - 1;
}

/*-------------------------------------------------------------
	Public: Set contrast
-------------------------------------------------------------*/
void ssd1306_SetContrast(uint8_t value) {
	ssd1306_WriteCommand(CONTRAST_CONTROL_REG);
	ssd1306_WriteCommand(value);
}

/*-------------------------------------------------------------
	Public: Initialize display
-------------------------------------------------------------*/
void ssd1306_Init(void) {

	Wire.begin();     /* Use default SDA/SCL for ESP8266 */

	delay(50);

	ssd1306_WriteCommand(0xAE);        /* Display off */

	ssd1306_WriteCommand(0x20);        /* Memory Addressing Mode */
	ssd1306_WriteCommand(0x00);        /* Page addressing mode */

	ssd1306_WriteCommand(0xB0);        /* Page Start Address */
	ssd1306_WriteCommand(0xC8);        /* COM Output Scan Direction remap */
	ssd1306_WriteCommand(0x00);        /* Low column start address */
	ssd1306_WriteCommand(0x10);        /* High column start address */
	ssd1306_WriteCommand(0x40);        /* Start line address */

	ssd1306_SetContrast(0xFF);         /* Max contrast */

	ssd1306_WriteCommand(0xA1);        /* Segment re-map */
	ssd1306_WriteCommand(0xA6);        /* Normal display (not inverted) */

	ssd1306_WriteCommand(0xA8);        /* Multiplex ratio */
	ssd1306_WriteCommand(0x3F);        /* 1/64 duty */

	ssd1306_WriteCommand(0xA4);        /* Output follows RAM content */

	ssd1306_WriteCommand(0xD3);        /* Display offset */
	ssd1306_WriteCommand(0x00);        /* No offset */

	ssd1306_WriteCommand(0xD5);        /* Display clock divide ratio/osc freq */
	ssd1306_WriteCommand(0xF0);

	ssd1306_WriteCommand(0xD9);        /* Pre-charge period */
	ssd1306_WriteCommand(0x22);

	ssd1306_WriteCommand(0xDA);        /* COM pins hardware configuration */
	ssd1306_WriteCommand(0x12);

	ssd1306_WriteCommand(0xDB);        /* VCOMH deselect level */
	ssd1306_WriteCommand(0x20);        /* ~0.77*Vcc */

	ssd1306_WriteCommand(0x8D);        /* Charge pump setting */
	ssd1306_WriteCommand(0x14);        /* Enable charge pump */

	ssd1306_WriteCommand(0xAF);        /* Display ON */

	/* Clear buffer and push once */
	ssd1306_FillScreen(Black);
	ssd1306_UpdateScreen();
}

/*-------------------------------------------------------------
	Public: Fill screen buffer
-------------------------------------------------------------*/
void ssd1306_FillScreen(SSD1306_COLOR color) {
	memset(SSD1306_Buffer, (color == White) ? 0xFF : 0x00, SSD1306_BUFFER_SIZE);
	ssd1306_MarkDirty(0, 0, SSD1306_WIDTH, SSD1306_HEIGHT);
}

/*-------------------------------------------------------------
	Public: Draw a single pixel into the buffer
-------------------------------------------------------------*/
void ssd1306_DrawPixel(uint8_t x, uint8_t y, SSD1306_COLOR color) {

	if (x >= SSD1306_WIDTH || y >= SSD1306_HEIGHT) {
		return;
	}

	uint16_t index = x + (y / 8) * SSD1306_WIDTH;
	uint8_t bit = y % 8;

	if (color == White) {
		SSD1306_Buffer[index] |= (1 << bit);
	}
	else {
		SSD1306_Buffer[index] &= ~(1 << bit);
	}

	ssd1306_MarkDirty(x, y, 1, 1);
}

/*-------------------------------------------------------------
	Public: Draw bitmap (vertical-byte format)
-------------------------------------------------------------*/
void ssd1306_DrawBitmap(uint8_t x, uint8_t y, const uint8_t* bitmap, uint8_t w, uint8_t h) {
	if (w == 0 || h == 0) return;

	uint8_t pages = h / 8;

	for (uint8_t page = 0; page < pages; page++)
	{
		for (uint8_t col = 0; col < w; col++)
		{
			uint16_t offset = (page * w) + col;
			uint8_t data = pgm_read_byte(bitmap + offset);

			for (uint8_t bit = 0; bit < 8; bit++)
			{
				uint8_t px = x + col;
				uint8_t py = y + (page * 8) + bit;

				// FULL CLIPPING — prevents ALL overflow
				if (px >= SSD1306_WIDTH)  continue;
				if (py >= SSD1306_HEIGHT) continue;

				if (data & (1 << bit))
					ssd1306_DrawPixel(px, py, White);
				else
					ssd1306_DrawPixel(px, py, Black);  // opaque bitmap
			}
		}
	}

	ssd1306_MarkDirty(x, y, w, h);
}
/*-------------------------------------------------------------
	Public: Draw raw bitmap (full screen)
	-------------------------------------------------------------*/
void ssd1306_DrawSplashRaw(const uint8_t* bitmap) {

	// Copy 128x64 = 1024 bytes of raw SSD1306 RAM
	for (uint16_t i = 0; i < SSD1306_BUFFER_SIZE; i++)
	{
		SSD1306_Buffer[i] = pgm_read_byte(bitmap + i);
	}

	// Mark whole screen dirty
	ssd1306_MarkDirty(0, 0, SSD1306_WIDTH, SSD1306_HEIGHT);
}

/*-------------------------------------------------------------
	Public: Update screen (only dirty rectangle)
-------------------------------------------------------------*/
void ssd1306_UpdateScreen(void) {

	if (!dirty) return;

	// Horizontal addressing: send full 128x64 buffer in one linear stream.

	// Column address: 0..127
	ssd1306_WriteCommand(0x21);
	ssd1306_WriteCommand(0x00);
	ssd1306_WriteCommand(SSD1306_WIDTH - 1);   // 127

	// Page address: 0..7
	ssd1306_WriteCommand(0x22);
	ssd1306_WriteCommand(0x00);
	ssd1306_WriteCommand((SSD1306_HEIGHT / 8) - 1);   // 7

	// Send all 1024 bytes. Chunk to keep Wire happy.
	uint16_t i = 0;
	while (i < SSD1306_BUFFER_SIZE) {

		Wire.beginTransmission(SSD1306_I2C_ADDR);
		Wire.write(0x40);  // data stream

		// send up to 16 bytes per I2C packet
		uint8_t chunk = 16;
		if (SSD1306_BUFFER_SIZE - i < chunk) {
			chunk = SSD1306_BUFFER_SIZE - i;
		}

		for (uint8_t c = 0; c < chunk; c++) {
			Wire.write(SSD1306_Buffer[i++]);
		}

		Wire.endTransmission();
	}

	dirty = false;
}

/*-------------------------------------------------------------
	Font helpers
-------------------------------------------------------------*/
static uint16_t getFontOffset(const uint8_t* font, char c) {
	uint8_t width = FONT_WIDTH(font);
	uint8_t height = FONT_HEIGHT(font);
	uint8_t first_char = FIRST_ASCII(font);

	uint8_t height_pages = height / 8;
	uint8_t index = (uint8_t)c - first_char;
	uint16_t bytesPerChar = width * height_pages;

	return 4 + (uint16_t)index * bytesPerChar;
}

/*-------------------------------------------------------------
	Public: Print a character using font
-------------------------------------------------------------*/
void ssd1306_PrintChar(uint8_t x, uint8_t r, char c, SSD1306_COLOR color, const uint8_t* font) {

	uint8_t width = FONT_WIDTH(font);
	uint8_t height = FONT_HEIGHT(font);
	uint8_t height_pages = height / 8;

	uint16_t offset = getFontOffset(font, c);

	uint8_t y = r * 8;   /* convert row to pixel y */

	for (uint8_t col = 0; col < width; col++) {
		for (uint8_t page = 0; page < height_pages; page++) {

			uint16_t idx = offset + col + (page * width);
			uint8_t  data = pgm_read_byte(font + idx);

			if (color == Black) {
				data = ~data;
			}

			for (uint8_t bit = 0; bit < 8; bit++) {
				uint8_t px = x + col;
				uint8_t py = y + (page * 8) + bit;

				if (data & (1 << bit)) {
					ssd1306_DrawPixel(px, py, White);
				}
				else {
					/* For opaque text, clear background:
					   ssd1306_DrawPixel(px, py, Black);
					   For transparent text, do nothing.
					*/
					ssd1306_DrawPixel(px, py, Black);
				}
			}
		}
	}

	ssd1306_MarkDirty(x, y, width, height);
}

/*-------------------------------------------------------------
	Public: Print string
	Returns: number of X pixels used
-------------------------------------------------------------*/
uint8_t ssd1306_PrintString(uint8_t x, uint8_t r, const char* str, SSD1306_COLOR color, const uint8_t* font) {
	uint8_t width = FONT_WIDTH(font);
	uint8_t startX = x;

	while (*str) {
		ssd1306_PrintChar(x, r, *str, color, font);
		x += width;
		str++;

		if (x >= SSD1306_WIDTH)
			break;
	}

	return (x - startX);
}

/*-------------------------------------------------------------
	Public: Print string centered horizontally
-------------------------------------------------------------*/
void ssd1306_PrintStringCentered(uint8_t r,
	const char* str,
	SSD1306_COLOR color,
	const uint8_t* font) {

	uint8_t width = FONT_WIDTH(font);
	uint8_t length = 0;

	while (str[length] != '\0') {
		length++;
	}

	uint8_t totalWidth = length * width;
	uint8_t x = 0;

	if (totalWidth < SSD1306_WIDTH) {
		x = (SSD1306_WIDTH - totalWidth) / 2;
	}

	ssd1306_PrintString(x, r, str, color, font);
}

/*-------------------------------------------------------------
	Public: Print number centered (0–255)
-------------------------------------------------------------*/
void ssd1306_PrintNumberCentered(uint8_t r,
	uint8_t number,
	SSD1306_COLOR color,
	const uint8_t* font) {

	char str[4];
	intToStr(number, str);
	ssd1306_PrintStringCentered(r, str, color, font);
}

/*-------------------------------------------------------------
	Public: int to string (0–255)
-------------------------------------------------------------*/
void intToStr(uint8_t num, char* str) {
	uint8_t i = 0;
	uint8_t tempNum = num;
	uint8_t digitCount = 0;

	do {
		digitCount++;
		tempNum /= 10;
	} while (tempNum);

	str[digitCount] = '\0';

	do {
		str[--digitCount] = (num % 10) + '0';
		num /= 10;
	} while (num);
}
/*-------------------------------------------------------------
	Public: float to string (0–6 decimal places)
-------------------------------------------------------------*/
void floatToStr(float value, char* str, uint8_t decimals) {

	if (decimals > 6) decimals = 6;

	// Handle negative numbers
	bool negative = false;
	if (value < 0.0f) {
		negative = true;
		value = -value;
	}

	// Extract integer part
	uint32_t intPart = (uint32_t)value;

	// Extract fractional part
	float frac = value - (float)intPart;

	// Scale fractional part
	uint32_t scale = 1;
	for (uint8_t i = 0; i < decimals; i++) {
		scale *= 10;
	}

	uint32_t fracPart = (uint32_t)(frac * scale + 0.5f);  // rounding

	// Build integer part using your intToStr
	char intBuf[12];
	intToStr(intPart, intBuf);

	// Start writing output
	char* p = str;

	// Add minus sign if needed
	if (negative) {
		*p++ = '-';
	}

	// Copy integer part
	for (uint8_t i = 0; intBuf[i] != '\0'; i++) {
		*p++ = intBuf[i];
	}

	// If no decimals, finish
	if (decimals == 0) {
		*p = '\0';
		return;
	}

	// Decimal point
	*p++ = '.';

	// Convert fractional part with leading zeros
	uint32_t divisor = scale / 10;
	for (uint8_t i = 0; i < decimals; i++) {
		uint8_t digit = (fracPart / divisor) % 10;
		*p++ = '0' + digit;
		divisor /= 10;
	}

	*p = '\0';
}
