#ifndef SOLDERDOODLE_TOUCH_SENSOR_H
#define SOLDERDOODLE_TOUCH_SENSOR_H

#include <stdint.h>
#include "const.h"

typedef enum {
	k_touch_off = 1,
	k_touch_start = 2,	// Touch started.
	k_touch_active = 4,	// A touch is active.
	k_touch_is_tap = 8,	// Active touch is tap/push.
	k_touch_is_drag = 16,	// Active touch is dragging.
	k_touch_end = 32,	// Touch ended.
	k_touch_taps = 64	// Multiple tap(s): Check tap_count.
} TouchStatus;

// Basic gesture recognition: Tap vs swipe.
typedef enum {
	k_gesture_unknown = 0,
	k_gesture_tap = 1,
	k_gesture_drag = 2
} TouchGesture;

struct TouchResult {
	TouchStatus status;
	int16_t dy;	// Change in drag position
} touch_result;

#define PRESSURE_SCALING_FACTOR_BITS   (11)

// Touch sensor: Max possible pressure value we care to handle.
#define MAX_PRESSURE                   (4095)

//const float PRESSURE_SMOOTHING = 0.5f;	// 0.0f: never changes. 1.0f: no smoothing

// Shared status
int16_t touch_pos = 0;
int16_t touch_pressure = 0;
bool is_touch_active = false;
int8_t tap_count = 0;

// Differentiate tap vs swipe:
// Track min & max Y position
const int16_t TOUCH_POS_UNDEFINED = INT16_MIN;
int16_t touch_pos_min = TOUCH_POS_UNDEFINED;
int16_t touch_pos_max = TOUCH_POS_UNDEFINED;

TouchGesture gesture = k_gesture_unknown;

// For taps: Record start & end times.
// Bucket brigade, most recent touch at index [0]:
uint32_t touch_start_times[MAX_TAPS];
uint32_t touch_end_times[MAX_TAPS];

void touch_reset_taps()
{
	for (int8_t i = 0; i < MAX_TAPS; i++) {
		touch_start_times[i] = 0;
		touch_end_times[i] = 0;
	}

	tap_count = 0;
}

void touch_sensor_init()
{
	touch_reset_taps();
}

int16_t get_touch_position()
{
	const uint8_t pins[] = {TOUCH_SL, TOUCH_D1, TOUCH_D2, TOUCH_RO};

	// Clear charge on the sensor.
	for (uint8_t i = 0; i < 4; i++) {
		pinMode(pins[i], OUTPUT);
		digitalWrite(pins[i], LOW);
	}

	// Set up appropriate driveline voltages
	digitalWrite(TOUCH_D1, HIGH);
	pinMode(TOUCH_RO, INPUT);
	pinMode(TOUCH_SL, INPUT);

	// Wait for voltage to stabilize
	delayMicroseconds(10);

	int result = analogRead(TOUCH_SL);
	return result;
}

// Returns int16_t in range: [0..MAX_PRESSURE]
// Soft pressure can drop to 5 or lower.
int16_t get_touch_pressure()
{
	// Set up appropriate drive line voltages
	pinMode(TOUCH_D1, OUTPUT);
	digitalWrite(TOUCH_D1, HIGH);
	pinMode(TOUCH_SL, INPUT);
	pinMode(TOUCH_D2, INPUT);
	pinMode(TOUCH_RO, OUTPUT);
	digitalWrite(TOUCH_RO, LOW);

	// Wait for voltage to stabilize
	delayMicroseconds(10);

	// Take two measurements
	int16_t v1 = analogRead(TOUCH_D2);
	int16_t v2 = analogRead(TOUCH_SL);

	// Calculate the pressure.
	// << 11: This is the "scaling factor" and is chosen kinda arbitrarily.
	// We want more touch sensitivity on the soft-touch end of the spectrum.
	int32_t v2_scaled = ((int32_t)(v2) << PRESSURE_SCALING_FACTOR_BITS);
	int32_t pressure = v2_scaled / max(v1 - v2, 1);

	return (int16_t)(constrain(pressure, 0, MAX_PRESSURE));
}

TouchResult * touch_tick(uint32_t now)
{
	int16_t last_touch_pos = touch_pos;
	touch_pos = get_touch_position();

	// Smooth the touch pressure a bit, try to de-noise the input
	touch_pressure = (get_touch_pressure() + touch_pressure) >> 1;

#	if DEV_PRINT_TOUCH
		if (touch_pressure > 0) {
			Serial.print("@ ");
			Serial.print(touch_pos);
			Serial.print(" -> ");
			Serial.println(touch_pressure);
		}
#	endif

	touch_result.dy = 0;	// default
	uint8_t status = 0;

	// Pressure is firm enough to be tap or swipe:
	if (touch_pressure >= PRESS_THRESHOLD_TAP) {

		// Movement: Only return .dy if we're confident this
		// is a drag.
		// * Gesture must be recognized as a drag.
		// * Pressure must be hard enough to drag.
		uint32_t touch_time = now - touch_start_times[0];

		bool do_track_position = (is_touch_active) && (touch_time >= DISCARD_MOVEMENT_FIRST_MILLIS);

		if (!do_track_position) {
			// Too early, so discard jittery movement.
			last_touch_pos = touch_pos;

		} else if (touch_pressure >= PRESS_THRESHOLD_DRAG) {
			touch_result.dy = touch_pos - last_touch_pos;

			// Tap vs swipe: Track the min & max Y values
			if (touch_pos_min == TOUCH_POS_UNDEFINED) {
				touch_pos_min = touch_pos;
				touch_pos_max = touch_pos;

			} else {
				touch_pos_min = min(touch_pos_min, touch_pos);
				touch_pos_max = max(touch_pos_max, touch_pos);
			}
		}

		// No touch active? Start tracking one now.
		if (!is_touch_active) {
			status |= k_touch_start;
			is_touch_active = true;
			gesture = k_gesture_unknown;

			// Before recording touch start: Shift all touch
			// times down (bucket brigade);
			for (int8_t t = MAX_TAPS - 1; t >= 1; t--) {
				touch_start_times[t] = touch_start_times[t - 1];
				touch_end_times[t] = touch_end_times[t - 1];
			}

			touch_start_times[0] = now;

		} else if (gesture == k_gesture_unknown) {
			// If drag distance is long enough:
			// It's definitely a drag.
			int16_t drag_distance = touch_pos_max - touch_pos_min;

			if (drag_distance >= GESTURE_DRAG_DISTANCE) {

				gesture = k_gesture_drag;

#				if DEV_PRINT_GESTURE_RECOG
					Serial.print("DRAG distance: ");
					Serial.println(drag_distance);
#				endif

			} else {
				if (touch_time >= TAP_RECOGNITION_MILLIS) {
					// Not much movement, so definitely a tap/push.
					gesture = k_gesture_tap;

#					if DEV_PRINT_GESTURE_RECOG
						Serial.print("tap. distance: ");
						Serial.println(drag_distance);
#					endif
				}
			}
		}

		// If we know tap vs drag: Return it.
		if (gesture == k_gesture_tap) {
			status |= k_touch_is_tap;
		} else if (gesture == k_gesture_drag) {
			status |= k_touch_is_drag;
		}

	} else {	// touch end
		if (is_touch_active) {
			status |= k_touch_end;
			is_touch_active = false;

			touch_end_times[0] = now;

#			if DEV_PRINT_TOUCH_MIN_MAX
				Serial.print(touch_pos_min);
				Serial.print(" <> ");
				Serial.print(touch_pos_max);
				Serial.print(" = ");
				Serial.println(touch_pos_max - touch_pos_min);
#			endif

			bool is_drag = (gesture == k_gesture_drag);
			if (!is_drag) {
				// Check min/max distance one last time.
				uint32_t drag_distance = touch_pos_max - touch_pos_min;
				if (drag_distance > GESTURE_DRAG_DISTANCE) {
					is_drag = true;
				}
			}

			if (is_drag) {
				// If this was a drag, clear the tap buffer,
				// and set tap_count to 0.
				touch_reset_taps();

			} else {
				// Tap event(s).
				tap_count = 0;

				for (int t = 0; t < MAX_TAPS; t++) {
					// Beyond the most recent tap: Is this tap close enough
					// to the previous one?
					if (t > 0) {
						uint32_t off_time = touch_end_times[t - 1] - touch_start_times[t];

						if ((off_time < TAP_TIME_MIN) || (TAP_TIME_MAX < off_time)) {
							break;
						}
					}

					uint32_t tap_time = touch_end_times[t] - touch_start_times[t];

					// Touch-active duration too short/long?
					// Then it's not a tap.
					if ((tap_time < TAP_TIME_MIN) || (TAP_TIME_MAX < tap_time)) {
						break;
					}

					// This was a tap.
					tap_count = t + 1;
					status |= k_touch_taps;
				}
			}

			// Reset touch min/max
			touch_pos_min = TOUCH_POS_UNDEFINED;
			touch_pos_max = TOUCH_POS_UNDEFINED;
		}
	}

	status |= (is_touch_active ? k_touch_active : k_touch_off);

	touch_result.status = status;

	return &touch_result;
}

#endif
