#include "st3m_usb.h"

static const char *TAG = "st3m-usb";

#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "freertos/semphr.h"

#include "esp_log.h"
#include "esp_mac.h"
#include "esp_private/usb_phy.h"
#include "tusb.h"

static SemaphoreHandle_t _mu = NULL;
static st3m_usb_mode_kind_t _mode = st3m_usb_mode_kind_disabled;
static usb_phy_handle_t phy_hdl;
static bool _connected = false;

static void _usb_task(void *_arg) {
	(void)_arg;
    while (true) {
  		tud_task_ext(10, false);

		xSemaphoreTake(_mu, portMAX_DELAY);
		st3m_usb_mode_kind_t mode = _mode;
		xSemaphoreGive(_mu);

		if (mode == st3m_usb_mode_kind_app) {
			st3m_usb_cdc_txpoll();
		}
    }
}

// Generate USB serial from on-board chip ID / MAC.
static void _generate_serial(void) {
	uint8_t mac[6];
	memset(mac, 0, 6);
	// Ignore error. Worst case we get zeroes.
	esp_read_mac(mac, ESP_MAC_WIFI_STA);

	char serial[13];
	snprintf(serial, 13, "%02x%02x%02x%02x%02x%02x", mac[0], mac[1], mac[2], mac[3], mac[4], mac[5]);
	st3m_usb_descriptors_set_serial(serial);
}

void st3m_usb_init(void) {
	assert(_mu == NULL);
	_mu = xSemaphoreCreateMutex();
	assert(_mu != NULL);

	_generate_serial();
	st3m_usb_cdc_init();
    usb_phy_config_t phy_conf = {
        .controller = USB_PHY_CTRL_OTG,
        .otg_mode = USB_OTG_MODE_DEVICE,
        .target = USB_PHY_TARGET_INT,
    };
	// TODO(q3k): set self-powered based on battery state?
    ESP_ERROR_CHECK(usb_new_phy(&phy_conf, &phy_hdl));

    if (!tusb_init()) {
        ESP_LOGE(TAG, "TInyUSB init failed");
        assert(false);
    }

   	xTaskCreate(_usb_task, "usb", 4096, NULL, 5, NULL);
	ESP_LOGI(TAG, "USB stack started");
}

void st3m_usb_mode_switch(st3m_usb_mode_t *mode) {
	xSemaphoreTake(_mu, portMAX_DELAY);

	bool running = false;
	switch (_mode) {
	case st3m_usb_mode_kind_app:
	case st3m_usb_mode_kind_disk:
		running = true;
	default:
	}

	bool should_run = false;
	switch (mode->kind) {
	case st3m_usb_mode_kind_app:
	case st3m_usb_mode_kind_disk:
		should_run = true;
	default:
	}

	if (running) {
		ESP_LOGI(TAG, "stopping and disconnecting");
        usb_phy_action(phy_hdl, USB_PHY_ACTION_HOST_FORCE_DISCONN);
		vTaskDelay(1000 / portTICK_PERIOD_MS);
	}

	st3m_usb_descriptors_switch(mode);
	if (mode->kind == st3m_usb_mode_kind_disk) {
		assert(mode->disk != NULL);
		st3m_usb_msc_set_conf(mode->disk);
	}
	if (mode->kind == st3m_usb_mode_kind_app) {
		assert(mode->app != NULL);
		st3m_usb_cdc_set_conf(mode->app);
	}

	if (should_run) {
		ESP_LOGI(TAG, "reconnecting and starting");
        usb_phy_action(phy_hdl, USB_PHY_ACTION_HOST_ALLOW_CONN);
	}

	_mode = mode->kind;
	_connected = false;
	xSemaphoreGive(_mu);
}

bool st3m_usb_connected(void) {
	xSemaphoreTake(_mu, portMAX_DELAY);
	bool res = _connected;
	xSemaphoreGive(_mu);
	return res;
}

void tud_mount_cb(void) {
	ESP_LOGI(TAG, "USB attached");
	xSemaphoreTake(_mu, portMAX_DELAY);
	_connected = true;
	xSemaphoreGive(_mu);
}

void tud_suspend_cb(bool remote_wakeup_en) {
	ESP_LOGI(TAG, "USB detached");

	xSemaphoreTake(_mu, portMAX_DELAY);
	st3m_usb_mode_kind_t mode = _mode;
	_connected = false;
	xSemaphoreGive(_mu);
	if (mode == st3m_usb_mode_kind_app) {
		st3m_usb_cdc_detached();
	}
}