#include "dlapi.h"

#include <Windows.h>

#include <exception>
#include <chrono>
#include <cinttypes>
#include <iostream>
#include <thread>
#include <string>

using namespace dl;
using namespace std::chrono_literals;

/*! A simple DLL lifetime management class which:
 *  - Loads DLAPI library into memory
 *  - Obtains interface creation endpoints 
 *  - Instantiates a Gateway interface
 *  - Cleans up on deletion
 */
class LibraryManager 
{
	HMODULE m_hLib;
	f_GetGateway m_fpGetGateway;
	f_DeleteGateway m_fpDeleteGateway;
	IGatewayPtr m_pGateway;

public:
	LibraryManager() 
		: m_hLib(nullptr)
		, m_fpGetGateway(nullptr)
		, m_fpDeleteGateway(nullptr) 
	{}

	~LibraryManager()
	{
		if (m_fpDeleteGateway) (*m_fpDeleteGateway)(m_pGateway);
		if (m_hLib) ::FreeLibrary(m_hLib);
		m_pGateway = nullptr;
	}

	IGatewayPtr initialize()
	{
		// Load DLAPI, and fail gracefully if not found
		auto hLib = ::LoadLibraryA("dlapi.dll");
		if (!hLib) throw std::logic_error("Could not find dlapi.dll. Make sure you have it installed in C:\\Windows\\SysWOW64");

		// Get the Gateway getters/deleters, and fail gracefully if not found
		m_fpGetGateway = (dl::f_GetGateway)GetProcAddress(hLib, "getGateway");
		m_fpDeleteGateway = (dl::f_DeleteGateway)GetProcAddress(hLib, "deleteGateway");

		if (!m_fpGetGateway) throw std::logic_error("Failed to find address of dl::GetGateway()");
		if (!m_fpDeleteGateway) throw std::logic_error("Failed to find address of dl::DeleteGateway()");

		// Get a Gateway interface
		m_pGateway = (*m_fpGetGateway)();
		return m_pGateway;
	}

	IGatewayPtr getGateway() const
	{
		return m_pGateway;
	}
};

std::string getSerial(ICameraPtr pCamera)
{
	char buf[512] = {0};
	size_t blng = 512;
	pCamera->getSerial(&(buf[0]), blng);
	return std::string(&(buf[0]), blng);
}

void handlePromise(IPromisePtr pPromise, std::string cmd = "", bool forceMsg = false)
{
	// Handle promises by waiting for them to report complete, or retrieving errors.
	// ALWAYS: release promises when you're done.
	auto result = pPromise->wait();
	if (forceMsg)
	{
		char b[512] = { 0 };
		size_t l = 512;
		pPromise->getLastError(&(b[0]), l);
		std::cout << cmd << " :: " << std::string(&(b[0]), l);
	}
	if (result != IPromise::Complete)
	{
		char b[512] = {0};
		size_t l = 512;
		pPromise->getLastError(&(b[0]), l);
		pPromise->release();
		throw std::logic_error(cmd + " :: " + std::string(&(b[0]), l));
	}
	pPromise->release();
}

int main() 
{
	try
	{
		// Load DLAPI dynamically, and manage its lifetime with the LibraryManager class.
		LibraryManager libMan;
		auto pGateway = libMan.initialize();

		// Query for cameras (blocking & without IPromise return)
		pGateway->queryUSBCameras();
		auto camCount = pGateway->getUSBCameraCount();
		if (camCount <= 0)
		{
			throw std::logic_error("No USB cameras found");
		}

		// If we found cameras, get the first one we found
		auto pCamera = pGateway->getUSBCamera(0);
		if (!pCamera)
		{
			throw std::logic_error("Camera could not be retrieved from Gateway");
		}

		// Initialize the camera (required as of DLAPI version 2.0 and above)
		pCamera->initialize();

		// Because the camera comes back auto-initialized, we can query it's serial number.
		// I've wrapped it in a convenience function to keep the code here moderately clean.
		auto serial = getSerial(pCamera);
		std::cout << "Serial: " << serial << std::endl;

		// And obtain the main imaging sensor of the camera (whose index is always 0)
		auto pSensor = pCamera->getSensor(0);
		if (!pSensor)
		{
			throw std::logic_error("Sensor could not be retrieved from Camera");
		}

		// Abort any lingering exposures from other applications (ALWAYS DO THIS ON CONNECT)
		handlePromise(pCamera->queryStatus(), "Camera::queryStatus");
		handlePromise(pSensor->abortExposure(), "Sensor::abortExposure");
		if (pCamera->getStatus().mainSensorState == ISensor::ReadyToDownload)
		{
			handlePromise(pSensor->startDownload());
		}

		// Retrieve the sensor info structure so we can store the dimensions of the camera
		// and set a full-frame subframe for exposure.
		auto sInfo = pSensor->getInfo();
		TSubframe sf;
		sf.binX = 1;
		sf.binY = 1;
		sf.top = 0;
		sf.left = 0;
		sf.width = sInfo.pixelsX;
		sf.height = sInfo.pixelsY;
		handlePromise(pSensor->setSubframe(sf));

		auto pFW = pCamera->getFW();
		if (!pFW) throw std::runtime_error("FW Could not be retrieved from Camera");

//		handlePromise(pFW->initialize(), "FW::Initialize");
//		handlePromise(pFW->setPosition(6), "FW::SetPosition");

		{
			using namespace std::chrono_literals;
			std::this_thread::sleep_for(100ms);
		}

		handlePromise(pSensor->setSetting(dl::ISensor::Setting::ContinuousExposure, 1), "SetSetting[ContinuousExposure]");

		// Preallocate buffers for test
		const int cFrames = 100;
		int frames = cFrames;
		size_t lng = sf.width * sf.height;
		uint16_t* pBuffer = new uint16_t[lng];
		ICamera::Status status;

		// Chronometry
		using hrc_t = std::chrono::high_resolution_clock;
		auto start = hrc_t::now();
		auto dts = new long long[frames];
		auto dts_ready = new long long[frames];
		auto dts_xferd = new long long[frames];
		auto dts_copyd = new long long[frames];

		// Start a 0.1 second, 1x1 binned, full frame dark frame.
		TExposureOptions eo;
		eo.binX = 1;
		eo.binY = 1;
		eo.duration = 0.1f;
		eo.isLightFrame = false;
		eo.readoutMode = 1;
		eo.useExtTrigger = false;
		eo.useRBIPreflash = false;
		std::cout << "Starting continuous exposures: " << sf.width << "x" << sf.height << ", "
			<< sf.binX << "x" << sf.binY << " binned, "
			<< eo.duration << " s, " << (eo.isLightFrame ? "Light" : "Dark") << " frame."
			<< std::endl;
		handlePromise(pSensor->startExposure(eo));

		
		hrc_t::time_point ready, xferd, copyd;
		while (--frames >= 0)
		{
			if (frames % 10 == 0) std::cout << "." << std::flush;
			start = hrc_t::now();

			// Wait until the camera reports the exposure on the main sensor is Ready to Download
			do
			{
				std::this_thread::sleep_for(1ms);
				handlePromise(pCamera->queryStatus());
				status = pCamera->getStatus();
			} while (status.mainSensorState != ISensor::ReadyToDownload);
			ready = hrc_t::now();

			std::this_thread::sleep_for(10ms);

			// Start the download, and wait for it to complete (this example is blocking, but there 
			// are non-blocking examples at https://diffractionlimited.com/dlapi/md_quickstart.html
			handlePromise(pSensor->startDownload()); // , "startDownload", true);
			xferd = hrc_t::now();

			// copy the image buffer out of DLAPI into local memory
			auto pImg = pSensor->getImage();
			auto pData = pImg->getBufferData();
			// lng = pImg->getBufferLength();
			memcpy(pBuffer, pData, sizeof(uint16_t) * lng);
			copyd = hrc_t::now();

			dts[frames] = std::chrono::duration_cast<std::chrono::microseconds>(hrc_t::now() - start).count();
			dts_ready[frames] = std::chrono::duration_cast<std::chrono::microseconds>(ready - start).count();
			dts_xferd[frames] = std::chrono::duration_cast<std::chrono::microseconds>(xferd - ready).count();
			dts_copyd[frames] = std::chrono::duration_cast<std::chrono::microseconds>(copyd - xferd).count();
		}

		std::cout << std::endl;
		handlePromise(pSensor->abortExposure());

		// Calculate the average ADU of the frame (should be approximately the bias level of 1000 ADU)
		double avg = 0;
		double avg_ready = 0, avg_xferd = 0, avg_copyd = 0;
		for (size_t i = 0; i < cFrames; i++)
		{
			avg += dts[i];
			avg_ready += dts_ready[i];
			avg_xferd += dts_xferd[i];
			avg_copyd += dts_copyd[i];
		}
		auto total = avg;
		avg /= cFrames;
		avg_ready /= cFrames;
		avg_xferd /= cFrames;
		avg_copyd /= cFrames;

		std::cout << "Average frame transfer time [us]: " << avg << std::endl;
		std::cout << "Average frame wait time [us]: " << avg_ready << std::endl;
		std::cout << "Average frame xfer time [us]: " << avg_xferd << std::endl;
		std::cout << "Average frame copy time [us]: " << avg_copyd << std::endl;
		std::cout << "FPS [Hz]: " << (1000000 * cFrames / total) << std::endl;

		// Clean up
		delete[] pBuffer;
		delete[] dts;
		delete[] dts_ready;
		delete[] dts_xferd;
		delete[] dts_copyd;

		// Note LibraryManager goes out of scope here and cleans up DLAPI for us, including the call to dl::deleteGateway()
	}
	catch(const std::exception& e)
	{
		// Report errors to user
		std::cerr << "\n" << e.what() << '\n';
	}

	// Wait for user input (Windows specific)
	std::system("pause");

	return 0;
}
