#pragma once

// The following ifdef block is the standard way of creating macros which make exporting
// from a DLL simpler. All files within this DLL are compiled with the PANDA_EXPORTS
// symbol defined on the command line. This symbol should not be defined on any project
// that uses this DLL. This way any other project whose source files include this file see
// PANDA_API functions as being imported from a DLL, whereas this DLL sees symbols
// defined with this macro as being exported.
#ifdef PANDA_EXPORTS
#define PANDA_API __declspec(dllexport)
#else
#define PANDA_API
#endif

#include <vector>
#include <string>
#include <unordered_map>
#include <memory>
#include <iostream>
#include <chrono>

#include <windows.h>
#include <winusb.h>

#if defined(UNICODE)
#define _tcout std::wcout
#define tstring std::wstring
#else
#define _tcout std::cout
#define tstring std::string
#endif

#define LIN_MSG_MAX_LEN 10
#define CAN_RX_QUEUE_LEN 10000
#define CAN_RX_MSG_LEN 1000

//template class __declspec(dllexport) std::basic_string<char>;

namespace panda {
	typedef enum _PANDA_SAFETY_MODE : uint16_t {
		SAFETY_NOOUTPUT = 0,
		SAFETY_HONDA = 1,
		SAFETY_ALLOUTPUT = 0x1337,
	} PANDA_SAFETY_MODE;

	typedef enum _PANDA_SERIAL_PORT : uint8_t {
		SERIAL_DEBUG = 0,
		SERIAL_ESP = 1,
		SERIAL_LIN1 = 2,
		SERIAL_LIN2 = 3,
	} PANDA_SERIAL_PORT;

	typedef enum _PANDA_SERIAL_PORT_PARITY : uint8_t {
		PANDA_PARITY_OFF = 0,
		PANDA_PARITY_EVEN = 1,
		PANDA_PARITY_ODD = 2,
	} PANDA_SERIAL_PORT_PARITY;

	typedef enum _PANDA_CAN_PORT : uint8_t {
		PANDA_CAN1 = 0,
		PANDA_CAN2 = 1,
		PANDA_CAN3 = 2,
		PANDA_CAN_UNK = 0xFF,
	} PANDA_CAN_PORT;

	typedef enum _PANDA_CAN_PORT_CLEAR : uint16_t {
		PANDA_CAN1_TX = 0,
		PANDA_CAN2_TX = 1,
		PANDA_CAN3_TX = 2,
		PANDA_CAN_RX = 0xFFFF,
	} PANDA_CAN_PORT_CLEAR;

	typedef enum _PANDA_GMLAN_HOST_PORT : uint8_t {
		PANDA_GMLAN_CLEAR = 0,
		PANDA_GMLAN_CAN2 = 1,
		PANDA_GMLAN_CAN3 = 2,
	} PANDA_GMLAN_HOST_PORT;

	#pragma pack(1)
	typedef struct _PANDA_HEALTH {
		uint32_t voltage;
		uint32_t current;
		uint8_t started;
		uint8_t controls_allowed;
		uint8_t gas_interceptor_detected;
		uint8_t started_signal_detected;
		uint8_t started_alt;
	} PANDA_HEALTH, *PPANDA_HEALTH;

	typedef struct _PANDA_CAN_MSG {
		uint32_t addr;
		unsigned long long recv_time; //In microseconds since device initialization
		std::chrono::time_point<std::chrono::steady_clock> recv_time_point;
		uint8_t dat[8];
		uint8_t len;
		PANDA_CAN_PORT bus;
		bool is_receipt;
		bool addr_29b;
	} PANDA_CAN_MSG;

	//Copied from https://stackoverflow.com/a/31488113
	class Timer
	{
		using clock = std::chrono::steady_clock;
		using time_point_type = std::chrono::time_point < clock, std::chrono::microseconds >;
	public:
		Timer() {
			start = std::chrono::time_point_cast<std::chrono::microseconds>(clock::now());
		}

		// gets the time elapsed from construction.
		unsigned long long /*microseconds*/ Timer::getTimePassedUS() {
			// get the new time
			auto end = std::chrono::time_point_cast<std::chrono::microseconds>(clock::now());

			// return the difference of the times
			return (end - start).count();
		}

		// gets the time elapsed from construction.
		unsigned long long /*milliseconds*/ Timer::getTimePassedMS() {
			// get the new time
			auto end = std::chrono::time_point_cast<std::chrono::milliseconds>(clock::now());

			// return the difference of the times
			auto startms = std::chrono::time_point_cast<std::chrono::milliseconds>(start);
			return (end - startms).count();
		}
	private:
		time_point_type start;
	};

	// This class is exported from the panda.dll
	class PANDA_API Panda {
	public:
		static std::vector<std::string> listAvailablePandas();
		static std::unique_ptr<Panda> openPanda(std::string sn);

		~Panda();

		std::string get_usb_sn();
		bool set_alt_setting(UCHAR alt_setting);
		UCHAR get_current_alt_setting();
		bool Panda::set_raw_io(bool val);

		PANDA_HEALTH get_health();
		bool enter_bootloader();
		std::string get_version();
		std::string get_serial();
		std::string get_secret();

		bool set_usb_power(bool on);
		bool set_esp_power(bool on);
		bool esp_reset(uint16_t bootmode);
		bool set_safety_mode(PANDA_SAFETY_MODE mode);
		bool set_can_forwarding(PANDA_CAN_PORT from_bus, PANDA_CAN_PORT to_bus);
		bool set_gmlan(PANDA_GMLAN_HOST_PORT bus);
		bool set_can_loopback(bool enable);
		bool set_can_speed_cbps(PANDA_CAN_PORT bus, uint16_t speed);
		bool set_can_speed_kbps(PANDA_CAN_PORT bus, uint16_t speed);
		bool set_uart_baud(PANDA_SERIAL_PORT uart, uint32_t rate);
		bool set_uart_parity(PANDA_SERIAL_PORT uart, PANDA_SERIAL_PORT_PARITY parity);

		bool can_send_many(const std::vector<PANDA_CAN_MSG>& can_msgs);
		bool can_send(uint32_t addr, bool addr_29b, const uint8_t *dat, uint8_t len, PANDA_CAN_PORT bus);
		std::vector<PANDA_CAN_MSG> can_recv();
		bool can_rx_q_push(HANDLE kill_event, DWORD timeoutms = INFINITE);
		void can_rx_q_pop(PANDA_CAN_MSG msg_out[], int &count);
		bool can_clear(PANDA_CAN_PORT_CLEAR bus);

		std::string serial_read(PANDA_SERIAL_PORT port_number);
		int serial_write(PANDA_SERIAL_PORT port_number, const void* buff, uint16_t len);
		bool serial_clear(PANDA_SERIAL_PORT port_number);
	private:
		Panda(
			WINUSB_INTERFACE_HANDLE WinusbHandle,
			HANDLE DeviceHandle,
			tstring devPath_,
			std::string sn_
		);

		int control_transfer(
			uint8_t bmRequestType,
			uint8_t bRequest,
			uint16_t wValue,
			uint16_t wIndex,
			void * data,
			uint16_t wLength,
			unsigned int timeout
		);

		int bulk_write(
			UCHAR endpoint,
			const void * buff,
			ULONG length,
			PULONG transferred,
			ULONG timeout
		);

		int Panda::bulk_read(
			UCHAR endpoint,
			void * buff,
			ULONG buff_size,
			PULONG transferred,
			ULONG timeout
		);

		#pragma pack(1)
		typedef struct _PANDA_CAN_MSG_INTERNAL {
			uint32_t rir;
			uint32_t f2;
			uint8_t dat[8];
		} PANDA_CAN_MSG_INTERNAL;

		typedef struct _CAN_RX_PIPE_READ {
			unsigned char data[sizeof(PANDA_CAN_MSG_INTERNAL) * CAN_RX_MSG_LEN];
			unsigned long count;
			OVERLAPPED overlapped;
			HANDLE complete;
			DWORD error;
		} CAN_RX_PIPE_READ;

		PANDA_CAN_MSG parse_can_recv(PANDA_CAN_MSG_INTERNAL *in_msg_raw);

		WINUSB_INTERFACE_HANDLE usbh;
		HANDLE devh;
		tstring devPath;
		std::string sn;
		bool loopback;

		Timer runningTime;
		CAN_RX_PIPE_READ can_rx_q[CAN_RX_QUEUE_LEN];
		unsigned long w_ptr = 0;
		unsigned long r_ptr = 0;
	};

}
