#include "../Interface/Server.h"
#include "../Interface/ThreadPool.h"
#include "../stringtools.h"
#include "ImdiskSrv.h"
#include "vhdfile.h"
#include "FileWrapper.h"
#include <Windows.h>
#include <Subauth.h>
#include <imdproxy.h>
#include <imdisk.h>
#include <string>
#include <algorithm>
#include <memory>

namespace
{
	HRESULT ModifyPrivilege(
		IN LPCTSTR szPrivilege,
		IN BOOL fEnable)
	{
		HRESULT hr = S_OK;
		TOKEN_PRIVILEGES NewState;
		LUID             luid;
		HANDLE hToken = NULL;

		// Open the process token for this process.
		if (!OpenProcessToken(GetCurrentProcess(),
			TOKEN_ADJUST_PRIVILEGES | TOKEN_QUERY,
			&hToken))
		{
			Server->Log("Failed OpenProcessToken", LL_ERROR);
			return ERROR_FUNCTION_FAILED;
		}

		// Get the local unique ID for the privilege.
		if (!LookupPrivilegeValue(NULL,
			szPrivilege,
			&luid))
		{
			CloseHandle(hToken);
			Server->Log("Failed LookupPrivilegeValue", LL_ERROR);
			return ERROR_FUNCTION_FAILED;
		}

		// Assign values to the TOKEN_PRIVILEGE structure.
		NewState.PrivilegeCount = 1;
		NewState.Privileges[0].Luid = luid;
		NewState.Privileges[0].Attributes =
			(fEnable ? SE_PRIVILEGE_ENABLED : 0);

		// Adjust the token privilege.
		if (!AdjustTokenPrivileges(hToken,
			FALSE,
			&NewState,
			0,
			NULL,
			NULL))
		{
			Server->Log("Failed AdjustTokenPrivileges", LL_ERROR);
			hr = ERROR_FUNCTION_FAILED;
		}

		// Close the handle.
		CloseHandle(hToken);

		return hr;
	}

	bool stopImdiskSvc()
	{
		SC_HANDLE sc = OpenSCManager(NULL, NULL,
			SC_MANAGER_ALL_ACCESS);

		if (sc == NULL)
		{
			return false;
		}

		SC_HANDLE service = OpenService(
			sc, L"ImDskSvc",
			SERVICE_STOP |	SERVICE_QUERY_STATUS |
			SERVICE_CHANGE_CONFIG);

		if (service == NULL)
		{
			CloseServiceHandle(sc);
			return false;
		}

		bool ret = true;
		do
		{
			SERVICE_STATUS ssp;
			if (!ControlService(service, SERVICE_CONTROL_STOP, &ssp))
			{
				ret = false;
			}

			if (!ChangeServiceConfig(service, SERVICE_NO_CHANGE, SERVICE_DEMAND_START, SERVICE_NO_CHANGE, NULL, NULL, NULL, NULL, NULL, NULL, NULL))
			{
				ret = false;
			}

		} while (0);

		CloseServiceHandle(sc);
		CloseServiceHandle(service);

		return ret;
	}

	class ScopedCloseHandle
	{
	public:
		ScopedCloseHandle(HANDLE h)
			: h(h) {}

		~ScopedCloseHandle()
		{
			if (h != INVALID_HANDLE_VALUE)
			{
				CloseHandle(h);
			}
		}

		void reset(HANDLE hNew = INVALID_HANDLE_VALUE)
		{
			if (h != INVALID_HANDLE_VALUE)
			{
				CloseHandle(h);
			}
			h = hNew;
		}

		void release()
		{
			h = INVALID_HANDLE_VALUE;
		}

	private:
		HANDLE h;
	};

	class ImdiskConnection : public IThread
	{
	public:
		ImdiskConnection(HANDLE hPipe)
			: hPipe(hPipe)
		{}

		~ImdiskConnection()
		{
			if (hPipe != INVALID_HANDLE_VALUE)
			{
				CloseHandle(hPipe);
			}
		}

		bool send_error(DWORD code)
		{
			IMDPROXY_CONNECT_RESP connect_resp = {};
			connect_resp.error_code = code;
			DWORD written;
			BOOL b = WriteFile(hPipe, &connect_resp, sizeof(connect_resp), &written, NULL);
			return b && written == sizeof(connect_resp);
		}

		void connect()
		{
			IMDPROXY_CONNECT_REQ connect_req;

			DWORD read;
			BOOL b = ReadFile(hPipe, &connect_req.request_code, sizeof(connect_req.request_code), &read, NULL);
			if (!b || read != sizeof(connect_req.request_code))
			{
				return;
			}
			if (connect_req.request_code != IMDPROXY_REQ_CONNECT)
			{
				return;
			}

			b = ReadFile(hPipe, reinterpret_cast<char*>(&connect_req) + sizeof(connect_req.request_code), 
				sizeof(connect_req) - sizeof(connect_req.request_code), &read, NULL);
			if (!b || read != sizeof(connect_req) - sizeof(connect_req.request_code))
			{
				return;
			}
			if (connect_req.length == 0 || connect_req.length > 520)
			{
				return;
			}
			std::wstring connect_string;
			connect_string.resize(connect_req.length);
			b = ReadFile(hPipe, &connect_string[0], static_cast<DWORD>(connect_req.length), &read, NULL);
			if (!b || read != connect_req.length)
			{
				return;
			}		

			if (IMDISK_PROXY_TYPE(connect_req.flags) != IMDISK_PROXY_TYPE_TCP)
			{
				send_error(ERROR_NOT_SUPPORTED);
				return;
			}

			size_t l_col = connect_string.find_last_of(':');

			int64 offset = 0;
			int64 length = -1;
			if (l_col > 3)
			{
				std::string slice = Server->ConvertFromWchar(connect_string.substr(l_col + 1));
				offset = watoi64(getuntil("-", slice));
				length = watoi64(getafter("-", slice));
				connect_string.erase(l_col, connect_string.size() - l_col);
			}
			else
			{
				offset = 512 * 1024;
			}

			std::string img_fn = Server->ConvertFromWchar(connect_string);

			std::string ext = findextension(img_fn);

			std::unique_ptr<IFile> imgf;

			std::unique_ptr<VHDFile> vhdfile;
			if (ext == "raw")
			{
				imgf.reset(Server->openFile(img_fn, MODE_READ));
			}
			else
			{
				vhdfile.reset(new VHDFile(img_fn, true, 0));
				if (vhdfile->isOpen())
				{
					imgf.reset(new FileWrapper(vhdfile.get(), 0));
				}
			}

			if(imgf.get()==NULL)
			{
				send_error(ERROR_FILE_NOT_FOUND);
				return;
			}

			if (length == -1)
			{
				length = imgf->Size() - offset;
			}
			else
			{
				length = (std::min)(length, imgf->Size() - offset);
			}

			std::string uid;
			uid.resize(16);
			Server->randomFill(&uid[0], uid.size());

			std::wstring pipe_name = L"\\\\.\\PIPE\\" + Server->ConvertToWchar(bytesToHex(uid));

			HANDLE hConnectPipe1 = CreateNamedPipe(pipe_name.c_str(),
				PIPE_ACCESS_DUPLEX | FILE_FLAG_WRITE_THROUGH | FILE_FLAG_OVERLAPPED, 0, PIPE_UNLIMITED_INSTANCES,
				0, 0, 0, NULL);

			if (hConnectPipe1 == INVALID_HANDLE_VALUE)
			{
				send_error(GetLastError());
				return;
			}

			ScopedCloseHandle closeConnectPipe1(hConnectPipe1);

			OVERLAPPED ovl = {};
			ovl.hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);

			ScopedCloseHandle closeEvent(ovl.hEvent);

			if (!ConnectNamedPipe(hConnectPipe1, &ovl)
				&& GetLastError() != ERROR_IO_PENDING)
			{
				send_error(GetLastError());
				return;
			}

			HANDLE hConnectPipe2 = CreateFile(pipe_name.c_str(),
				GENERIC_READ | GENERIC_WRITE, 0, NULL,
				OPEN_EXISTING, 0, NULL);

			if (hConnectPipe2 == INVALID_HANDLE_VALUE)
			{
				send_error(GetLastError());
				return;
			}

			ScopedCloseHandle closeConnectPipe2(hConnectPipe2);

			if (WaitForSingleObject(ovl.hEvent, INFINITE) != WAIT_OBJECT_0)
			{
				send_error(ERROR_CONNECTION_ABORTED);
				return;
			}

			closeEvent.reset();

			IMDPROXY_CONNECT_RESP connect_resp = {};

			{
				HANDLE hDriver = CreateFile(IMDISK_CTL_DOSDEV_NAME,
					GENERIC_READ | GENERIC_WRITE,
					FILE_SHARE_READ | FILE_SHARE_WRITE,
					NULL,
					OPEN_EXISTING,
					0,
					NULL);

				if (hDriver == INVALID_HANDLE_VALUE)
				{
					send_error(GetLastError());
					return;
				}

				ScopedCloseHandle closeDriver(hDriver);

				DWORD bytes_returned;
				if (!DeviceIoControl(hDriver, IOCTL_IMDISK_REFERENCE_HANDLE,
					&hConnectPipe1, sizeof(hConnectPipe1), &connect_resp.object_ptr,
					sizeof(connect_resp.object_ptr), &bytes_returned, NULL))
				{
					send_error(GetLastError());
					return;
				}
			}

			DWORD written;
			b = WriteFile(hPipe, &connect_resp, sizeof(connect_resp), &written, NULL);
			if (!b || written != sizeof(connect_resp))
			{
				return;
			}

			std::vector<char> buf;

			while (true)
			{
				ULONGLONG request_code;
				b = ReadFile(hConnectPipe2, &request_code, sizeof(request_code), &read, NULL);
				if (!b || read != sizeof(request_code))
				{
					return;
				}

				if (request_code == IMDPROXY_REQ_INFO)
				{
					IMDPROXY_INFO_RESP info_resp;
					info_resp.file_size = length;
					info_resp.flags = IMDPROXY_FLAG_RO;
					info_resp.req_alignment = 512;

					b = WriteFile(hConnectPipe2, &info_resp, sizeof(info_resp), &written, NULL);

					if (!b || written != sizeof(info_resp))
					{
						Server->Log("Error sending info response. Err: " + convert((int64)GetLastError()), LL_ERROR);
						break;
					}
					continue;
				}
				else if (request_code != IMDPROXY_REQ_READ)
				{
					Server->Log("Unsupported ImDisk request code "+ convert(request_code), LL_ERROR);

					ULONGLONG resp = ENODEV;
					WriteFile(hConnectPipe2, &resp, sizeof(resp), &written, NULL);
					return;
				}
				
				IMDPROXY_READ_REQ read_req = {};

				b = ReadFile(hConnectPipe2, &read_req.offset, sizeof(read_req) - sizeof(request_code), &read, NULL);
				if (!b || read!=sizeof(read_req)-sizeof(request_code))
				{
					Server->Log("Reading read request failed. Err: " + convert((int64)GetLastError()), LL_ERROR);
					break;
				}

				if (buf.size() < read_req.length + sizeof(IMDPROXY_READ_RESP))
				{
					buf.resize(read_req.length + sizeof(IMDPROXY_READ_RESP));
				}

				//Server->Log("Read req offset=" + convert(read_req.offset) + " length=" + convert(read_req.length), LL_INFO);

				IMDPROXY_READ_RESP* read_resp = reinterpret_cast<IMDPROXY_READ_RESP*>(buf.data());
				read_resp->length = 0;
				read_resp->errorno = 0;

				if (read_req.length > 0)
				{
					bool has_read_error = false;
					_u32 rc = imgf->Read(read_req.offset+offset, buf.data() + sizeof(IMDPROXY_READ_RESP)+ read_resp->length,
						static_cast<_u32>(read_req.length), &has_read_error);
					if (has_read_error)
					{
						read_resp->errorno = EIO;
						read_resp->length = 0;
						rc = 0;
					}
					else
					{
						read_resp->length += rc;
					}
				}

				//Server->Log("Read resp length=" + convert(read_resp->length), LL_INFO);
				b = WriteFile(hConnectPipe2, read_resp, static_cast<DWORD>(sizeof(IMDPROXY_READ_RESP) + read_resp->length), &written, NULL);
				if (!b || written != sizeof(IMDPROXY_READ_RESP) + read_resp->length)
				{
					Server->Log("Sending read response failed. Err: " + convert((int64)GetLastError()), LL_ERROR);
					break;
				}
			}
		}

		void operator()()
		{
			connect();			
			delete this;
		}

	private:
		HANDLE hPipe;
	};
}


ImdiskSrv::ImdiskSrv()
{
	
}

bool ImdiskSrv::installed()
{
	HANDLE hDriver = CreateFile(IMDISK_CTL_DOSDEV_NAME,
		GENERIC_READ | GENERIC_WRITE,
		FILE_SHARE_READ | FILE_SHARE_WRITE,
		NULL,
		OPEN_EXISTING,
		0,
		NULL);

	if (hDriver != INVALID_HANDLE_VALUE)
	{
		CloseHandle(hDriver);
	}

	return hDriver != INVALID_HANDLE_VALUE;
}

void ImdiskSrv::operator()()
{
	HRESULT hr = ModifyPrivilege(SE_TCB_NAME, TRUE);
	if (!SUCCEEDED(hr))
	{
		Server->Log("Failed to modify SE_TCB privilege. VHD mounting won't work.", LL_ERROR);
		return;
	}

	stopImdiskSvc();

	while (true)
	{
		HANDLE hNamedPipe = CreateNamedPipe(IMDPROXY_SVC_PIPE_DOSDEV_NAME,
			PIPE_ACCESS_DUPLEX | FILE_FLAG_WRITE_THROUGH,
			0,
			PIPE_UNLIMITED_INSTANCES,
			0, 0, 0, NULL);

		if (hNamedPipe == INVALID_HANDLE_VALUE)
		{
			Server->Log("Error creating named pipe to ImProxy. Err: " + convert((int64)GetLastError()), LL_ERROR);
			return;
		}

		if (!ConnectNamedPipe(hNamedPipe, NULL))
		{
			Server->Log("Error connecting named pipe to ImProxy. Err: " + convert((int64)GetLastError()), LL_ERROR);
			CloseHandle(hNamedPipe);
			return;
		}

		Server->createThread(new ImdiskConnection(hNamedPipe), "imdisk conn");
	}
}
