/*
      LOOKING AT THE PLUG FROM CONTROLLER
          /---------\
 PIN 1-> /  o  o  o  \
        /-------------\


PIN # USAGE

    GND
    DATA
    VCC +3.3V ONLY
*/

#include "Arduino.h"

#include "pins.h"

#define DATA_PIN OR_PIN_2

//#define PRINT_Y_AXIS_VALUES 1
//#define PRINT_X_AXIS_VALUES 1
//#define PLOT_CONSOLE_POLLING 1
//#define DEBUG
//#define PRINT_DATA

#ifndef GAMEPAD_COUNT
#define GAMEPAD_COUNT 1
#endif

#define AXIS_CENTER_IN 0
#define AXIS_MAX_IN 70
#define AXIS_MIN_IN -70

#include "gamepad/Gamepad.h"
#include "util.cpp"

#define LINE_WRITE_HIGH pinMode(DATA_PIN, INPUT_PULLUP)
#define LINE_WRITE_LOW pinMode(DATA_PIN, OUTPUT)

#define DATA_SIZE 2048	// number of sample points to poll
#define CALIBRATE_PASSES 5

#define NUM_BITS 32

// buffer to hold data being read from controller
bool buffer[DATA_SIZE];

// bit resolution
int bitResolution = 0;
int lastIndex = 0;

bool returnedBits[NUM_BITS];
bool oldReport[NUM_BITS];

GAMEPAD_CLASS gamepad;

struct ControllerData {
	bool buttonA;
	bool buttonB;
	bool buttonZ;
	bool buttonL;
	bool buttonR;
	bool buttonStart;

	bool DPadUp;
	bool DPadDown;
	bool DPadLeft;
	bool DPadRight;

	bool CUp;
	bool CDown;
	bool CLeft;
	bool CRight;

	short xAxis;
	short yAxis;

	bool xAxisRaw[8];
	bool yAxisRaw[8];
};

void updateOffsetsAndResolution() {
	// the current bit counter
	int bitCounter = 0;

	// to hold the number of 1's in this bit
	int thisResolution = 0;

	// current index
	int i = 0;

	bitResolution /= 2;

	for (; i < DATA_SIZE; i++) {
		if (buffer[i] == false) {
			// we skip all leading 1's
			break;
		}
	}

	// iterate over buffer
	for (; i < DATA_SIZE - 1 && bitCounter < NUM_BITS; i++) {
		if (buffer[i] == true) {
			++thisResolution;
			// if a falling edge is detected
			if (buffer[1 + i] == false) {
				// store bit's earliest possible beginning offsets
				if (lastIndex < (i + 1)) {
					lastIndex = i + 1;
				}

				// store max resolution in bitResolution
				if (thisResolution > bitResolution) {
					bitResolution = thisResolution;
				}

				// reset thisResolution
				thisResolution = 0;
				// increment bitCounter
				++bitCounter;
			}
		}
	}

	bitResolution *= 2;
}

void checkBitsToRead() {
	if (lastIndex < NUM_BITS * 2) {
		// not enough, error out...
		while (true) {
			printf("not enough bitsToRead, increase DATA_SIZE ???\n");
			delay(5000);
		}
	}
}

/** Function to send a Command to the attached N64-Controller.
 *  Must be run from RAM to defy timing differences introduced from
 *  reading Code from ESP32's SPI Flash Chip.
*/
void IRAM_ATTR sendCommand(byte command) {
	// the current bit to write
	bool bit;

	noInterrupts();

	// for each bit
	for (int i = 0; i < 8; i++) {
		// get value
		bit = (1 << (7 - i)) & command;

		// write data
		LINE_WRITE_LOW;
		delayMicroseconds((3 - 2 * bit));
		LINE_WRITE_HIGH;
		delayMicroseconds((1 + 2 * bit));
	}

	// console stop bit
	LINE_WRITE_LOW;
	delayMicroseconds(1);
	LINE_WRITE_HIGH;
	delayMicroseconds(1);  // by spec should be 2

	// read returned data as fast as possible
	for (int i = 0; i < DATA_SIZE; i++) {
		// this is faster:
#if defined(ARDUINO_ARCH_ESP32)

#if DATA_PIN < 32
		buffer[i] = (GPIO.in >> DATA_PIN) & 0x1;
#elif DATA_PIN < 40
		buffer[i] = (GPIO.in1.val >> (DATA_PIN - 32)) & 0x1;
#else

#error unsupported DATA_PIN must be <40

#endif

#else
		// this is portable:
		buffer[i] = digitalRead(DATA_PIN);
#endif	// not defined(ARDUINO_ARCH_ESP32)
	}

	interrupts();

// plot polling process from controller if unstructed to
#ifdef PLOT_CONSOLE_POLLING
	for (int i = 0; i < DATA_SIZE; i++) {
		Serial.println(buffer[i] * 2500);
	}

#endif

#ifdef PRINT_DATA
	for (int i = 0; i < DATA_SIZE; i++) {
		printf(buffer[i] ? "1" : "0");
	}
	printf("\n----------------\n");
#endif
}

bool calcReturnedBits() {
	// the current bit counter
	int bitCounter = 0;

	// to hold the number of 1's in this bit
	int thisResolution = 0;

	// current index
	int i = 0;
	for (; i < DATA_SIZE; i++) {
		if (buffer[i] == false) {
			// we skip all leading 1's
			break;
		}
	}

	// iterate over buffer
	for (; i < DATA_SIZE - 1 && bitCounter < NUM_BITS; i++) {
		if (buffer[i] == true) {
			++thisResolution;
			// if a falling edge is detected
			if (buffer[1 + i] == false) {
				returnedBits[bitCounter] = thisResolution >= bitResolution;

				// reset thisResolution
				thisResolution = 0;
				// increment bitCounter
				++bitCounter;
			}
		}
	}

#ifdef DEBUG
	if (bitCounter != 32) {
		printf("%i != 32, not enough bitsToRead, increase DATA_SIZE ???\n", bitCounter);
		// not enough, error out...
		/*
        for (int i = 0; i < bitsToRead; i++) {
            printf(buffer[i] ? "1" : "0");
        }
        printf("\n----------------\n");
	*/
	}
#endif

	return bitCounter == 32;
}

/** Function to populate the controller struct if command 0x01 was sent.
 *  Buttons are set according to data in buffer, raw axis data is written,
 *  Axis Data is correctly decoded from raw axis data by taking two's complement
 *  and checking if value if below 'MAX_INCLINE_AXIS_X' or 'MAX_INCLINE_AXIS_Y'.
 *  If values surpass the maximum incline they are set to match those values.
 */
void populateControllerStruct(ControllerData *data) {
	// first byte
	data->buttonA = returnedBits[0];
	data->buttonB = returnedBits[1];
	data->buttonZ = returnedBits[2];
	data->buttonStart = returnedBits[3];
	data->DPadUp = returnedBits[4];
	data->DPadDown = returnedBits[5];
	data->DPadLeft = returnedBits[6];
	data->DPadRight = returnedBits[7];

	// second byte, first two bits are unused
	data->buttonL = returnedBits[10];
	data->buttonR = returnedBits[11];
	data->CUp = returnedBits[12];
	data->CDown = returnedBits[13];
	data->CLeft = returnedBits[14];
	data->CRight = returnedBits[15];

	for (int i = 0; i < 8; ++i) {
		// third byte
		data->xAxisRaw[i] = returnedBits[i + 16];
		// fourth byte
		data->yAxisRaw[i] = returnedBits[i + 24];
	}

	// sum up bits to get axis bytes
	data->xAxis = 0;
	data->yAxis = 0;
	for (int i = 0; i < 8; i++) {
		data->xAxis += (data->xAxisRaw[i] * (0x80 >> (i)));
		data->yAxis += (data->yAxisRaw[i] * (0x80 >> (i)));
	}

// print axis values
#ifdef DEBUG
	Serial.printf("yRaw: %i %i %i %i %i %i %i %i xRaw: %i %i %i %i %i %i %i %i yAxis: %03i xAxis: %03i",
				  data->yAxisRaw[0],
				  data->yAxisRaw[1],
				  data->yAxisRaw[2],
				  data->yAxisRaw[3],
				  data->yAxisRaw[4],
				  data->yAxisRaw[5],
				  data->yAxisRaw[6],
				  data->yAxisRaw[7],
				  data->xAxisRaw[0],
				  data->xAxisRaw[1],
				  data->xAxisRaw[2],
				  data->xAxisRaw[3],
				  data->xAxisRaw[4],
				  data->xAxisRaw[5],
				  data->xAxisRaw[6],
				  data->xAxisRaw[7],
				  data->yAxis,
				  data->xAxis);
#endif

	// decode xAxis two's complement
	if (data->xAxis & 0x80) {
		data->xAxis = -1 * (0xff - data->xAxis);
	}

	// decode yAxis two's complement
	if (data->yAxis & 0x80) {
		data->yAxis = -1 * (0xff - data->yAxis);
	}

	// keep x axis below maxIncline
	if (data->xAxis > AXIS_MAX_IN)
		data->xAxis = AXIS_MAX_IN;
	if (data->xAxis < AXIS_MIN_IN)
		data->xAxis = AXIS_MIN_IN;

	// keep y axis below maxIncline
	if (data->yAxis > AXIS_MAX_IN)
		data->yAxis = AXIS_MAX_IN;
	if (data->yAxis < AXIS_MIN_IN)
		data->yAxis = AXIS_MIN_IN;

	//Serial.printf("xaxis: %-3i yaxis: %-3i \n",data->xAxis,data->yAxis);
}

ControllerData controller;

void setup() {
#ifdef DEBUG
	Serial.begin(115200);
	delay(5000);
#endif

	gamepad.begin();

	// setup io pins
	//setupIO();
	// the controller data line
	LINE_WRITE_HIGH;

#ifdef PLOT_CONSOLE_POLLING
	sendCommand(0x01);
	while (true)
		;
#endif

	for (int i = 0; i < CALIBRATE_PASSES; ++i) {
		sendCommand(0x01);
		updateOffsetsAndResolution();
		if (i != CALIBRATE_PASSES) {
			delay(14);
		}
	}
	checkBitsToRead();
#ifdef DEBUG
	printf("bitResolution: %i, lastIndex: %i\n", bitResolution, lastIndex);
	delay(5000);
#endif
}

void loop() {
	// polling must not occur faster than every 20 ms
	delay(14);

	// send command 0x01 to n64 controller
	sendCommand(0x01);

	if (calcReturnedBits() && memcmp(oldReport, returnedBits, sizeof(returnedBits))) {
		memcpy(oldReport, returnedBits, sizeof(returnedBits));
	} else {
		// nothing changed
		return;
	}

	// store received data in controller struct
	populateControllerStruct(&controller);

	uint8_t c = 0;	// for now just do 1 pad
	gamepad.buttons(c, 0);
	if (controller.buttonStart) {
		if (controller.DPadDown) {
			// then only send menu, nothing else
			gamepad.press(c, BUTTON_MENU);
			gamepad.setHatSync(c, DPAD_CENTER);
			return;
		}
		gamepad.press(c, BUTTON_START);
	}
	if (controller.buttonA) {
		gamepad.press(c, BUTTON_A);
	}
	if (controller.buttonB) {
		gamepad.press(c, BUTTON_B);
	}
	if (controller.buttonZ) {
		gamepad.press(c, BUTTON_TR);
	}
	if (controller.buttonL) {
		gamepad.press(c, BUTTON_L);
	}
	if (controller.buttonR) {
		gamepad.press(c, BUTTON_R);
	}
	auto hat = calculateDpadDirection(controller.DPadUp, controller.DPadDown, controller.DPadLeft, controller.DPadRight);
	auto cHat = dpadToAxis(calculateDpadDirection(controller.CUp, controller.CDown, controller.CLeft, controller.CRight));
	gamepad.setAxis(c, translateAxis(controller.xAxis), -translateAxis(controller.yAxis), cHat.x, cHat.y, 0, 0, hat);

#ifdef DEBUG
	/*
	for (int i = 0; i < bitsToRead; i++) {
		Serial.print(buffer[i] ? "1" : "0");
	}
	Serial.println("\n----------------");
*/

	Serial.print(" buttons: ");
	Serial.print(controller.buttonA ? "A" : "-");
	Serial.print(controller.buttonB ? "B" : "-");
	Serial.print(controller.buttonZ ? "Z" : "-");
	Serial.print(controller.buttonL ? "L" : "-");
	Serial.print(controller.buttonR ? "R" : "-");
	Serial.print(controller.buttonStart ? "S" : "-");
	Serial.print(" DPAD: ");
	Serial.print(controller.DPadUp ? "U" : "-");
	Serial.print(controller.DPadDown ? "D" : "-");
	Serial.print(controller.DPadLeft ? "L" : "-");
	Serial.print(controller.DPadRight ? "R" : "-");
	Serial.print(" C: ");
	Serial.print(controller.CUp ? "U" : "-");
	Serial.print(controller.CDown ? "D" : "-");
	Serial.print(controller.CLeft ? "L" : "-");
	Serial.print(controller.CRight ? "R" : "-");
	Serial.print(" Y: ");
	Serial.print(controller.yAxis);
	Serial.print(" YT: ");
	Serial.print(-translateAxis(controller.yAxis));
	Serial.print(" X: ");
	Serial.print(controller.xAxis);
	Serial.print(" XT: ");
	Serial.print(translateAxis(controller.xAxis));
	Serial.println();
#endif
}
