#include "Core.h"

namespace Upp {

namespace Ini {
	INI_BOOL(HttpRequest_Trace, false, "Activates HTTP requests tracing")
	INI_BOOL(HttpRequest_TraceBody, false, "Activates HTTP requests body tracing")
	INI_BOOL(HttpRequest_TraceShort, false, "Activates HTTP requests short tracing")
};

#define LLOG(x)      LOG_(Ini::HttpRequest_Trace, x)
#define LLOGB(x)     LOG_(Ini::HttpRequest_TraceBody, x)
#define LLOGS(x)     LOG_( Ini::HttpRequest_Trace || Ini::HttpRequest_TraceShort, x)
#define LLOGSS(x)    LOG_(!Ini::HttpRequest_Trace && Ini::HttpRequest_TraceShort, x)
	
#ifdef _DEBUG
_DBG_
// #define ENDZIP // only activate if zip pipe is in the question
#endif

void HttpRequest::Trace(bool b)
{
	Ini::HttpRequest_Trace = b;
	Ini::HttpRequest_TraceBody = b;
}

void HttpRequest::TraceHeader(bool b)
{
	Ini::HttpRequest_Trace = b;
}

void HttpRequest::TraceBody(bool b)
{
	Ini::HttpRequest_TraceBody = b;
}

void HttpRequest::TraceShort(bool b)
{
	Ini::HttpRequest_TraceShort = b;
}

void HttpRequest::Init()
{
	port = 0;
	proxy_port = 0;
	ssl_proxy_port = 0;
	max_header_size = 1000000;
	max_content_size = 10000000;
	max_redirects = 10;
	max_retries = 3;
	force_digest = false;
	std_headers = true;
	hasurlvar = false;
	keep_alive = false;
	method = METHOD_GET;
	phase = BEGIN;
	redirect_count = 0;
	retry_count = 0;
	gzip = false;
	all_content = false;
	WhenAuthenticate = callback(this, &HttpRequest::ResolveDigestAuthentication);
	chunk = 4096;
	timeout = 120000;
	ssl = false;
	poststream = NULL;
	postlen = Null;
	has_content_length = false;
	chunked_encoding = false;
}

HttpRequest::HttpRequest()
{
	Init();
}

HttpRequest::HttpRequest(const char *url)
{
	Init();
	Url(url);
}

HttpRequest& HttpRequest::Method(int m, const char *custom_name)
{
	method = m;
	custom_method = custom_name;
	return *this;
}

HttpRequest& HttpRequest::Url(const char *u)
{
	ssl = memcmp(u, "https", 5) == 0;
	const char *t = u;
	while(*t && *t != '?')
		if(*t++ == '/' && *t == '/') {
			u = ++t;
			break;
		}
	t = u;
	while(*u && *u != ':' && *u != '/' && *u != '?')
		u++;
	hasurlvar = *u == '?' && u[1];
	host = String(t, u);
	port = 0;
	if(*u == ':')
		port = ScanInt(u + 1, &u);
	path = u;
	int q = path.Find('#');
	if(q >= 0)
		path.Trim(q);
	return *this;
}

void ParseProxyUrl(const char *p, String& proxy_host, int& proxy_port)
{
	const char *t = p;
	while(*p && *p != ':')
		p++;
	proxy_host = String(t, p);
	if(*p++ == ':' && IsDigit(*p))
		proxy_port = ScanInt(p);
}

HttpRequest& HttpRequest::Proxy(const char *url)
{
	proxy_port = 80;
	ParseProxyUrl(url, proxy_host, proxy_port);
	return *this;
}

HttpRequest& HttpRequest::SSLProxy(const char *url)
{
	ssl_proxy_port = 8080;
	ParseProxyUrl(url, ssl_proxy_host, ssl_proxy_port);
	return *this;
}

HttpRequest& HttpRequest::PostStream(Stream& s, int64 len)
{
	POST();
	poststream = &s;
	postlen = Nvl(len, s.GetLeft());
	postdata.Clear();
	return *this;
}

HttpRequest& HttpRequest::Post(const char *id, const String& data)
{
	POST();
	if(postdata.GetCount())
		postdata << '&';
	postdata << id << '=' << UrlEncode(data);
	return *this;
}

HttpRequest& HttpRequest::Part(const char *id, const String& data,
                               const char *content_type, const char *filename)
{
	if(IsNull(multipart)) {
		POST();
		multipart = AsString(Uuid::Create());
		ContentType("multipart/form-data; boundary=" + multipart);
	}
	postdata << "--" << multipart << "\r\n"
	         << "Content-Disposition: form-data; name=\"" << id << "\"";
	if(filename && *filename)
		postdata << "; filename=\"" << filename << "\"";
	postdata << "\r\n";
	if(content_type && *content_type)
		postdata << "Content-Type: " << content_type << "\r\n";
	postdata << "\r\n" << data << "\r\n";
	return *this;
}

HttpRequest& HttpRequest::UrlVar(const char *id, const String& data)
{
	int c = *path.Last();
	if(hasurlvar && c != '&')
		path << '&';
	if(!hasurlvar && c != '?')
		path << '?';
	path << id << '=' << UrlEncode(data);
	hasurlvar = true;
	return *this;
}

String HttpRequest::CalculateDigest(const String& authenticate) const
{
	const char *p = authenticate;
	String realm, qop, nonce, opaque;
	while(*p) {
		if(!IsAlNum(*p)) {
			p++;
			continue;
		}
		else {
			const char *b = p;
			while(IsAlNum(*p))
				p++;
			String var = ToLower(String(b, p));
			String value;
			while(*p && (byte)*p <= ' ')
				p++;
			if(*p == '=') {
				p++;
				while(*p && (byte)*p <= ' ')
					p++;
				if(*p == '\"') {
					p++;
					while(*p && *p != '\"')
						if(*p != '\\' || *++p)
							value.Cat(*p++);
					if(*p == '\"')
						p++;
				}
				else {
					b = p;
					while(*p && *p != ',' && (byte)*p > ' ')
						p++;
					value = String(b, p);
				}
			}
			if(var == "realm")
				realm = value;
			else if(var == "qop")
				qop = value;
			else if(var == "nonce")
				nonce = value;
			else if(var == "opaque")
				opaque = value;
		}
	}
	String hv1, hv2;
	hv1 << username << ':' << realm << ':' << password;
	String ha1 = MD5String(hv1);
	hv2 << (method == METHOD_GET ? "GET" : method == METHOD_PUT ? "PUT" : method == METHOD_POST ? "POST" : "READ")
	<< ':' << path;
	String ha2 = MD5String(hv2);
	int nc = 1;
	String cnonce = FormatIntHex(Random(), 8);
	String hv;
	hv << ha1
	   << ':' << nonce
	   << ':' << FormatIntHex(nc, 8)
	   << ':' << cnonce
	   << ':' << qop << ':' << ha2;
	String ha = MD5String(hv);
	String auth;
	auth << "username=" << AsCString(username)
	     << ", realm=" << AsCString(realm)
	     << ", nonce=" << AsCString(nonce)
	     << ", uri=" << AsCString(path)
	     << ", qop=" << AsCString(qop)
	     << ", nc=" << AsCString(FormatIntHex(nc, 8))
	     << ", cnonce=" << cnonce
	     << ", response=" << AsCString(ha);
	if(!IsNull(opaque))
		auth << ", opaque=" << AsCString(opaque);
	return auth;
}

HttpRequest& HttpRequest::Header(const char *id, const String& data)
{
	request_headers << id << ": " << data << "\r\n";
	return *this;
}

HttpRequest& HttpRequest::Cookie(const HttpCookie& c)
{
	cookies.GetAdd(String(c.id).Cat() << '?' << c.domain << '?' << c.path) = c;
	return *this;
}

HttpRequest& HttpRequest::Cookie(const String& id, const String& value, const String& domain, const String& path)
{
	HttpCookie c;
	c.id = id;
	c.value = value;
	c.domain = domain;
	c.path = path;
	return Cookie(c);
}

HttpRequest& HttpRequest::CopyCookies(const HttpRequest& r)
{
	const HttpHeader& h = r.GetHttpHeader();
	for(int i = 0; i < h.cookies.GetCount(); i++)
		Cookie(h.cookies[i]);
	return *this;
}

void HttpRequest::HttpError(const char *s)
{
	if(IsError())
		return;
	error = NFormat(t_("%s:%d: ") + String(s), host, port);
	LLOGS("HTTP ERROR: " << error);
	Close();
}

void HttpRequest::StartPhase(int s)
{
	phase = s;
	LLOG("Starting status " << s << " '" << GetPhaseName() << "', url: " << host);
	data.Clear();
}

void HttpRequest::New()
{
	ClearError();
	ClearAbort();
	phase = BEGIN;
}

void HttpRequest::NewRequest()
{
	New();
	Init();
	host = proxy_host = proxy_username = proxy_password = ssl_proxy_host =
	ssl_proxy_username = ssl_proxy_password = path =
	custom_method = accept = agent = contenttype = username = password =
	authorization = request_headers = postdata = multipart = Null;
}

void HttpRequest::Clear()
{
	TcpSocket::Clear();
	NewRequest();
	cookies.Clear();
}

bool HttpRequest::Do()
{
	switch(phase) {
	case BEGIN:
		retry_count = 0;
		redirect_count = 0;
		start_time = msecs();
		GlobalTimeout(timeout);
	case START:
		Start();
		break;
	case DNS:
		Dns();
		break;
	case SSLPROXYREQUEST:
		if(SendingData())
			break;
		StartPhase(SSLPROXYRESPONSE);
		break;
	case SSLPROXYRESPONSE:
		if(ReadingHeader())
			break;
		ProcessSSLProxyResponse();
		break;
	case SSLHANDSHAKE:
		if(SSLHandshake())
			break;
		StartRequest();
		break;
	case REQUEST:
		if(SendingData(true))
			break;
		StartPhase(HEADER);
		break;
	case HEADER:
		if(ReadingHeader())
			break;
		StartBody();
		break;
	case BODY:
		if(ReadingBody())
			break;
		Finish();
		break;
	case CHUNK_HEADER:
		ReadingChunkHeader();
		break;
	case CHUNK_BODY:
		if(ReadingBody())
			break;
		StartPhase(CHUNK_CRLF);
		break;
	case CHUNK_CRLF:
		if(chunk_crlf.GetCount() < 2)
			chunk_crlf.Cat(TcpSocket::Get(2 - chunk_crlf.GetCount()));
		if(chunk_crlf.GetCount() < 2)
			break;
		if(chunk_crlf != "\r\n")
			HttpError("missing ending CRLF in chunked transfer");
		StartPhase(CHUNK_HEADER);
		break;
	case TRAILER:
		if(ReadingHeader())
			break;
		header.ParseAdd(data);
		Finish();
		break;
	case FINISHED:
	case FAILED:
		WhenDo();
		return false;
	default:
		NEVER();
	}

	if(phase != FAILED) {
		if(IsSocketError() || IsError())
			phase = FAILED;
		else
		if(msecs(start_time) >= timeout) {
			HttpError("connection timed out");
			phase = FAILED;
		}
		else
		if(IsAbort()) {
			HttpError("connection was aborted");
			phase = FAILED;
		}
	}
	
	if(phase == FAILED) {
		if(retry_count++ < max_retries) {
			LLOGS("HTTP retry on error " << GetErrorDesc());
			start_time = msecs();
			GlobalTimeout(timeout);
			StartPhase(START);
		}
	}
	WhenDo();
	return phase != FINISHED && phase != FAILED;
}

void HttpRequest::Start()
{
	LLOG("HTTP START");
	Close();
	ClearError();
	gzip = false;
	z.Clear();
	header.Clear();
	status_code = 0;
	reason_phrase.Clear();
	body.Clear();
	WhenStart();

	bool use_proxy = !IsNull(ssl ? ssl_proxy_host : proxy_host);

	int p = use_proxy ? (ssl ? ssl_proxy_port : proxy_port) : port;
	if(!p)
		p = ssl ? DEFAULT_HTTPS_PORT : DEFAULT_HTTP_PORT;
	String h = use_proxy ? ssl ? ssl_proxy_host : proxy_host : host;
	LLOG("Using " << (use_proxy ? "proxy " : "") << h << ":" << p);
	
	SSLServerNameIndication(host);

	StartPhase(DNS);
	if(IsNull(GetTimeout()) && timeout == INT_MAX) {
		if(WhenWait) {
			addrinfo.Start(h, p);
			while(addrinfo.InProgress()) {
				Sleep(GetWaitStep());
				WhenWait();
				if(msecs(start_time) >= timeout)
					break;
			}
		}
		else
			addrinfo.Execute(h, p);
		StartConnect();
	}
	else
		addrinfo.Start(h, p);
}

void HttpRequest::Dns()
{
	for(int i = 0; i <= Nvl(GetTimeout(), INT_MAX); i++) {
		if(!addrinfo.InProgress()) {
			StartConnect();
			return;
		}
		Sleep(1);
		if(msecs(start_time) >= timeout)
			break;
	}
}

void HttpRequest::StartConnect()
{
	LLOG("HTTP StartConnect");
	if(!Connect(addrinfo))
		return;
	addrinfo.Clear();
	if(ssl && ssl_proxy_host.GetCount()) {
		StartPhase(SSLPROXYREQUEST);
		String host_port = host;
		if(port)
			host_port << ':' << port;
		else
			host_port << ":443";
		data << "CONNECT " << host_port << " HTTP/1.1\r\n"
		     << "Host: " << host_port << "\r\n";
		if(!IsNull(ssl_proxy_username))
			data << "Proxy-Authorization: Basic "
			        << Base64Encode(proxy_username + ':' + proxy_password) << "\r\n";
		data << "\r\n";
		count = 0;
		LLOG("HTTPS proxy request:\n" << data);
	}
	else
		AfterConnect();
}

void HttpRequest::ProcessSSLProxyResponse()
{
	LLOG("HTTPS proxy response:\n" << data);
	int q = min(data.Find('\r'), data.Find('\n'));
	if(q >= 0)
		data.Trim(q);
	if(!data.StartsWith("HTTP") || data.Find(" 2") < 0) {
		HttpError("Invalid proxy reply: " + data);
		return;
	}
	AfterConnect();
}

void HttpRequest::AfterConnect()
{
	LLOG("HTTP AfterConnect");
	if(ssl && !StartSSL())
		return;
	if(ssl)
		StartPhase(SSLHANDSHAKE);
	else
		StartRequest();
}

void HttpRequest::StartRequest()
{
	StartPhase(REQUEST);
	count = 0;
	String ctype = contenttype;
	if((method == METHOD_POST || method == METHOD_PUT) && IsNull(ctype))
		ctype = "application/x-www-form-urlencoded";
	static const char *smethod[] = {
		"GET", "POST", "HEAD", "PUT", "DELETE", "TRACE", "OPTIONS", "CONNECT", "PATCH",
	};
	ASSERT(method >= 0 && method <= 8);
	data = Nvl(custom_method, smethod[method]);
	data << ' ';
	String host_port = host;
	if(port)
		host_port << ':' << port;
	String url;
	url << "http://" << host_port << Nvl(path, "/");
	if(!IsNull(proxy_host) && !ssl)
		data << url;
	else {
		if(*path != '/')
			data << '/';
		data << path;
	}
	data << " HTTP/1.1\r\n";
	String pd = postdata;

	if(!IsNull(multipart))
		pd << "--" << multipart << "--\r\n";
	if(method == METHOD_GET || method == METHOD_HEAD) {
		pd.Clear();
		poststream = NULL;
	}
	if(std_headers) {
		data << "URL: " << url << "\r\n"
		     << "Host: " << host_port << "\r\n"
		     << "Connection: " << (keep_alive ? "keep-alive\r\n" : "close\r\n")
		     << "Accept: " << Nvl(accept, "*/*") << "\r\n"
		     << "Accept-Encoding: gzip\r\n"
		     << "User-Agent: " << Nvl(agent, "U++ HTTP request") << "\r\n";
		int64 len = poststream ? postlen : pd.GetCount();
		if(len > 0 || method == METHOD_POST || method == METHOD_PUT)
			data << "Content-Length: " << len << "\r\n";
		if(ctype.GetCount())
			data << "Content-Type: " << ctype << "\r\n";
	}
	VectorMap<String, Tuple<String, int>> cms;
	for(int i = 0; i < cookies.GetCount(); i++) {
		const HttpCookie& c = cookies[i];
		if(host.EndsWith(c.domain) && path.StartsWith(c.path)) {
			Tuple<String, int>& m = cms.GetAdd(c.id, MakeTuple(String(), -1));
			if(c.path.GetLength() > m.b) {
				m.a = c.value;
				m.b = c.path.GetLength();
			}
		}
	}
	String cs;
	for(int i = 0; i < cms.GetCount(); i++) {
		if(i)
			cs << "; ";
		cs << cms.GetKey(i) << '=' << cms[i].a;
	}
	if(cs.GetCount())
		data << "Cookie: " << cs << "\r\n";
	if(!IsNull(proxy_host) && !IsNull(proxy_username))
		 data << "Proxy-Authorization: Basic " << Base64Encode(proxy_username + ':' + proxy_password) << "\r\n";
	if(!IsNull(authorization))
		data << "Authorization: " << authorization << "\r\n";
	else
	if(!force_digest && (!IsNull(username) || !IsNull(password)))
		data << "Authorization: Basic " << Base64Encode(username + ":" + password) << "\r\n";
	data << request_headers;
	LLOG("HTTP REQUEST " << host << ":" << port);
	if (pd.GetCount() || method == METHOD_POST || method == METHOD_PUT)
	    LLOGSS("HTTP Request " << smethod[method] << " " << url << " data:" << ctype << "(" << pd.GetCount() << ")");
	else
	    LLOGSS("HTTP Request " << smethod[method] << " " << url);
	LLOG("HTTP request:\n" << data);
	data << "\r\n" << pd;
	LLOGB("HTTP request body:\n" << pd);
}

bool HttpRequest::SendingData(bool request)
{
	const int upload_chunk =  64*1024;

	if(count < data.GetLength())
		for(;;) {
			int n = min(upload_chunk, data.GetLength() - (int)count);
			n = TcpSocket::Put(~data + count, n);
			if(n == 0) {
				if(count < data.GetLength())
					return true;
				if(poststream && request)
					break;
				return false;
			}
			count += n;
		}
	if(poststream && request)
		for(;;) {
			Buffer<byte> buffer(upload_chunk);
			int n = poststream->Get(buffer, (int)min((int64)upload_chunk, postlen + data.GetLength() - count));
			if(n < 0) {
				HttpError("error reading input stream");
				return false;
			}
			if(n == 0)
				break;
			n = TcpSocket::Put(buffer, n);
			if(n == 0)
				break;
			count += n;
		}
	return count < data.GetLength() + postlen;
}

bool HttpRequest::ReadingHeader()
{
	for(;;) {
		int c = TcpSocket::Get();
		if(c < 0)
			return !IsEof();
		else
			data.Cat(c);
		if(data.GetCount() == 2 && data[0] == '\r' && data[1] == '\n') // header is empty
			return false;
		if(data.GetCount() >= 3) {
			const char *h = data.Last();
			if(h[0] == '\n' && h[-1] == '\r' && h[-2] == '\n') // empty ending line after non-empty header
				return false;
		}
		if(data.GetCount() > max_header_size) {
			HttpError("HTTP header exceeded " + AsString(max_header_size));
			return true;
		}
	}
}

void HttpRequest::ReadingChunkHeader()
{
	for(;;) {
		int c = TcpSocket::Get();
		if(c < 0)
			break;
		else
		if(c == '\n') {
			int n = ScanInt(~data, NULL, 16);
			LLOG("HTTP Chunk header: 0x" << data << " = " << n);
			if(IsNull(n)) {
				HttpError("invalid chunk header");
				break;
			}
			if(n == 0) {
				StartPhase(TRAILER);
				break;
			}
			count += n;
			StartPhase(CHUNK_BODY);
			chunk_crlf.Clear();
			break;
		}
		if(c != '\r')
			data.Cat(c);
	}
}

String HttpRequest::GetRedirectUrl()
{
	String redirect_url = TrimLeft(header["location"]);
	if(redirect_url.StartsWith("http://") || redirect_url.StartsWith("https://"))
		return redirect_url;
	String h = (ssl ? "https://" : "http://") + host;
	if(*redirect_url != '/')
		h << '/';
	h << redirect_url;
	return h;
}

int64 HttpRequest::GetContentLength()
{
	return header.GetContentLength();
}

void HttpRequest::StartBody()
{
	LLOG("HTTP Header received: ");
	LLOG(data);
	header.Clear();
	if(!header.Parse(data)) {
		HttpError("invalid HTTP header");
		return;
	}
	
	if(!header.Response(protocol, status_code, reason_phrase)) {
		HttpError("invalid HTTP response");
		return;
	}
	
	LLOG("HTTP status code: " << status_code);

	count = GetContentLength();
	has_content_length = count;
	
	if(method == METHOD_HEAD)
		phase = FINISHED;
	else
	if(header["transfer-encoding"] == "chunked") {
		count = 0;
		chunked_encoding = true;
		StartPhase(CHUNK_HEADER);
	}
	else
		StartPhase(BODY);
	body.Clear();
	gzip = GetHeader("content-encoding") == "gzip";
	if(gzip) {
		gzip = true;
		z.WhenOut = callback(this, &HttpRequest::Out);
		z.ChunkSize(chunk).GZip().Decompress();
	}
}

void HttpRequest::Out(const void *ptr, int size)
{
	LLOG("HTTP Out " << size);
	if(z.IsError())
		HttpError("gzip format error");
	if(body.GetCount() + size > max_content_size) {
		HttpError("content length exceeded " + AsString(max_content_size));
		phase = FAILED;
		return;
	}
	if(WhenContent && (status_code >= 200 && status_code < 300 || all_content))
		WhenContent(ptr, size);
	else
		body.Cat((const char *)ptr, size);
}


bool HttpRequest::ReadingBody()
{
	LLOG("HTTP reading body " << count);
	String s = TcpSocket::Get(has_content_length || chunked_encoding ? (int)min((int64)chunk, count)
	                                                                 : chunk);
	if(s.GetCount()) {
	#ifndef ENDZIP
		if(gzip)
			z.Put(~s, s.GetCount());
		else
	#endif
			Out(~s, s.GetCount());
		if(count > 0) {
			count -= s.GetCount();
			return !IsEof() && count > 0;
		}
	}
	return !IsEof();
}
/*
bool HttpRequest::ReadingBody()
{
	LLOG("HTTP reading body " << count);
	String s = TcpSocket::Get((int)min((int64)chunk, count));
	if(s.GetCount() == 0)
		return !IsEof() && count;
#ifndef ENDZIP
	if(gzip)
		z.Put(~s, s.GetCount());
	else
#endif
		Out(~s, s.GetCount());
	if(count > 0) {
		count -= s.GetCount();
		return !IsEof() && count > 0;
	}
	return !IsEof();
}
*/

void HttpRequest::CopyCookies()
{
	CopyCookies(*this);
}

bool HttpRequest::ResolveDigestAuthentication()
{
	String authenticate = header["www-authenticate"];
	if(authenticate.StartsWith("Digest")) {
		Digest(CalculateDigest(authenticate));
		return true;
	}
	return false;
}

void HttpRequest::Finish()
{
	if(gzip) {
	#ifdef ENDZIP
		body = GZDecompress(body);
		if(body.IsVoid()) {
			HttpError("gzip decompress at finish error");
			phase = FAILED;
			return;
		}
	#else
		z.End();
		if(z.IsError()) {
			HttpError("gzip format error (finish)");
			phase = FAILED;
			return;
		}
	#endif
	}
	CopyCookies();
	if(status_code == 401 && redirect_count++ < max_redirects && WhenAuthenticate()) {
		if(keep_alive)
			StartRequest();
		else
			Start();
		return;
	}
	Close();
	if(status_code >= 300 && status_code < 400) {
		String url = GetRedirectUrl();
		GET();
		if(url.GetCount() && redirect_count++ < max_redirects) {
			LLOG("--- HTTP redirect " << url);
			Url(url);
			Start();
			retry_count = 0;
			return;
		}
	}
	phase = FINISHED;
}

String HttpRequest::Execute()
{
	New();
	while(Do())
		LLOG("HTTP Execute: " << GetPhaseName());
	LLOGSS("HTTP Reply: " << status_code << " " << reason_phrase <<" size:" << GetContent().GetCount() << " type:" << GetHeader("content-type"));
	return IsSuccess() ? GetContent() : String::GetVoid();
}

String HttpRequest::GetPhaseName() const
{
	static const char *m[] = {
		"Initial state",
		"Start",
		"Resolving host name",
		"SSL proxy request",
		"SSL proxy response",
		"SSL handshake",
		"Sending request",
		"Receiving header",
		"Receiving content",
		"Receiving chunk header",
		"Receiving content chunk",
		"Receiving content chunk ending",
		"Receiving trailer",
		"Request with continue",
		"Waiting for continue header",
		"Finished",
		"Failed",
	};
	return phase >= 0 && phase <= FAILED ? m[phase] : "";
}

}
