#include "stdafx.h"
#include "IORequest.h"
#include "Thread.h"

#if defined(POSIX)
#include <sys/socket.h>
#endif

namespace os {

#if defined(WINDOWS)

	IORequest::IORequest(Handle handle, const Thread &thread, nat timeout)
		: wake(0), handle(handle), bytes(0), error(0), thread(thread) {

		Internal = 0;
		InternalHigh = 0;
		Offset = 0;
		OffsetHigh = 0;
		Pointer = NULL;
		hEvent = NULL;
		thread.threadData()->ioComplete.attach();

		if (timeout) {
			sleep.until = UThreadState::sleepTarget(timeout);
			sleep.request = this;
			thread.threadData()->uState.addSleep(&sleep);
		}
	}

	IORequest::~IORequest() {
		if (sleep.request)
			thread.threadData()->uState.cancelSleep(&sleep);
		thread.threadData()->ioComplete.detach();
	}

	void IORequest::complete(nat bytes) {
		this->bytes = bytes;
		this->error = 0;
		wake.up();
	}

	void IORequest::failed(nat bytes, int error) {
		this->bytes = bytes;
		this->error = error;
		wake.up();
	}

	void IOTimeoutSleep::signal() {
		if (request) {
			// This causes this particular IO request to be cancelled as normal. That means we don't
			// have to do anything special to handle it, even if it was partially completed.
			CancelIoEx(request->handle.v(), request);
		}
		request = null;
	}

#elif defined(POSIX)

	IORequest::IORequest(Operation op, Handle handle, const Thread &thread)
		: thread(thread), result(0), timeoutSent(false), deallocate(false) {

		zeroMem(request);
		request.opcode = op;
		request.fd = handle.v();
	}

	IORequest::~IORequest() {
		if (sleep.request)
			thread.threadData()->uState.cancelSleep(&sleep);
		io().detach(Handle(request.fd), this);
	}

	int IORequest::submit(nat timeout) {
		io().attach(Handle(request.fd), this);
		if (timeout) {
			sleep.until = UThreadState::sleepTarget(timeout);
			sleep.request = this;
			thread.threadData()->uState.addSleep(&sleep);
		}
		wake.wait();
		return result;
	}

	int IORequest::submitAndDetach(nat timeout) {
		io().attachAndRemove(Handle(request.fd), this);
		if (timeout) {
			sleep.until = UThreadState::sleepTarget(timeout);
			sleep.request = this;
			thread.threadData()->uState.addSleep(&sleep);
		}
		wake.wait();
		return result;
	}

	void IORequest::submitDetachDelete(nat timeout) {
		deallocate = true;
		io().attachAndRemove(Handle(request.fd), this);
		if (timeout) {
			sleep.until = UThreadState::sleepTarget(timeout);
			sleep.request = this;
			thread.threadData()->uState.addSleep(&sleep);
		}
	}

	IOHandle &IORequest::io() {
		return thread.threadData()->ioComplete;
	}

	void IORequest::onFinish(int *result) {
		if (result) {
			this->result = *result;
		}

		bool deallocate = this->deallocate;
		wake.set();

		// Note: We don't touch 'this' after calling 'set'. If we are stack allocated by another
		// thread, we might be deallocated by now.
		if (deallocate)
			delete this;
	}

	void IOTimeoutSleep::signal() {
		if (request) {
			// Ask to cancel.
			request->timeoutSent = true;
			request->io().cancel(request);
		}
		request = null;
	}

	IORequest::DoResult IORequest::doRequest() {
		while (true) {
			switch (request.opcode) {
			case READ: {
				ssize_t r = ::read(request.fd, (void *)request.addr, request.len);
				if (r >= 0) {
					result = int(r);
					return doDone;
				} else if (errno == EAGAIN || errno == EWOULDBLOCK) {
					return doWaitRead;
				} else if (errno != EINTR) {
					result = -errno;
					return doDone;
				}
				break;
			}
			case WRITE: {
				ssize_t r = ::write(request.fd, (const void *)request.addr, request.len);
				if (r >= 0) {
					result = int(r);
					return doDone;
				} else if (errno == EAGAIN || errno == EWOULDBLOCK) {
					return doWaitWrite;
				} else if (errno != EINTR) {
					result = -errno;
					return doDone;
				}
				break;
			}
			case OPENAT:
				result = ::openat(request.fd, (const char *)request.addr, request.open_flags, (mode_t)request.len);
				if (result >= 0) {
					return doDone;
				} else if (errno != EINTR) {
					result = -errno;
					return doDone;
				}
				break;
			case CLOSE:
				result = ::close(request.fd);
				if (result >= 0) {
					return doDone;
				} else if (errno != EINTR) {
					result = -errno;
					return doDone;
				}
				break;
			case STATX:
				result = ::statx(request.fd, (const char *)request.addr,
							request.statx_flags, request.len, (struct statx *)request.off);
				if (result >= 0) {
					return doDone;
				} else if (errno != EINTR) {
					result = -errno;
					return doDone;
				}
				break;
			case UNLINKAT:
				result = ::unlinkat(request.fd, (const char *)request.addr, request.unlink_flags);
				if (result >= 0) {
					return doDone;
				} else if (errno != EINTR) {
					result = -errno;
					return doDone;
				}
				break;
			case MKDIRAT:
				result = ::mkdirat(request.fd, (const char *)request.addr, (mode_t)request.len);
				if (result >= 0) {
					return doDone;
				} else if (errno != EINTR) {
					result = -errno;
					return doDone;
				}
				break;
			case FSYNC:
				if (request.fsync_flags != 0) {
					result = ::fdatasync(request.fd);
				} else {
					result = ::fsync(request.fd);
				}
				if (result >= 0) {
					return doDone;
				} else if (errno != EINTR) {
					result = -errno;
					return doDone;
				}
				break;
			case ACCEPT: {
				int r = ::accept(request.fd, (struct sockaddr *)request.addr, (socklen_t *)request.addr2);
				if (r >= 0) {
					result = r;
					return doDone;
				} else if (errno == EAGAIN || errno == EWOULDBLOCK) {
					return doWaitRead;
				} else if (errno != EINTR) {
					result = -errno;
					return doDone;
				}
				break;
			}
			case CONNECT: {
				// Connect is a bit weird. You are supposed to call 'connect' once, then just wait
				// until select indicates that it is done. Otherwise you get EISCONN. So, we set
				// addr to 1 when we have called connect once.
				if (request.addr != 1) {
					// Call connect:
					int r = ::connect(request.fd, (struct sockaddr *)request.addr, (socklen_t)request.off);
					if (r >= 0) {
						// Completed directly, nothing to worry about.
						result = r;
						return doDone;
					} else if (errno == EINPROGRESS || errno == EALREADY) {
						// Connection in progress, wait for it to continue. Set addr to 1 to
						// remember that we should not call connect again.
						request.addr = 1;
						return doWaitWrite;
					} else if (errno != EINTR) {
						// Error, return directly.
						result = -errno;
						return doDone;
					}
				} else {
					// We called connect before. If we get here, we should just return and say that we are happy.
					int error = 0;
					socklen_t len = sizeof(int);
					if (getsockopt(request.fd, SOL_SOCKET, SO_ERROR, &error, &len)) {
						perror("getsockopt");
						return doDone;
					}
					// Note: handles the case of error = 0 (i.e. success) as well.
					result = -error;
					return doDone;
				}
				break;
			}
			default:
				assert(false, L"Unsupported request!");
				result = -EINVAL;
				return doDone;
			}
		}
	}

#endif

}
