# Author: Scott Woods <scott.18.ansar@gmail.com>
# MIT License
#
# Copyright (c) 2017-2023 Scott Woods
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
__docformat__ = 'restructuredtext'

import string
import uuid
from urllib.parse import quote_plus, unquote_plus
import ansar.create as ar
from .transporting_if import ts

from copy import copy

__all__ = [
	'HttpRequest',
	'HttpResponse',
	'stream_form',
	'recover_form',
	'ApiServerStream',
	'ApiClientStream',
	'ApiClientSession',
]

# Inbound to API server
# or outbound from client.
class HttpRequest(object):
	"""The message generated by an HTTP client and received by an HTTP server.

	:param method: one of GET, POST, PUT, ...
	:type method: str
	:param request_uri: the path section of a URI
	:type request_uri: str
	:param http: version of HTTP
	:type http: str
	:param header: name-value pairs
	:type header: dict
	:param body: payload, value
	:type body: bytearray
	"""
	def __init__(self, method=None, request_uri=None, http=None, header=None, body=None):
		self.method = method or 'POST'
		self.request_uri = request_uri or '/'
		self.http = http or 'HTTP/1.1'
		self.header = header or {}
		self.body = body

HTTP_REQUEST_SCHEMA = {
	'method': ar.Unicode(),
	'request_uri': ar.Unicode(),
	'http': ar.Unicode(),
	'header': ar.MapOf(ar.Unicode(),ar.Unicode()),
	'body': ar.Block(),
}

# Outbound from API server
# or inbound to client.
class HttpResponse(object):
	"""The message generated by an HTTP server and received by an HTTP client.

	:param http: version of HTTP
	:type http: str
	:param status_code: 100, 200, 300, ...
	:type status_code: int
	:param reason_phrase: short description
	:type reason_phrase: str
	:param header: name-value pairs
	:type header: dict
	:param body: payload, value
	:type body: bytearray
	"""
	def __init__(self, http=None, status_code=200, reason_phrase=None, header=None, body=None,
			plain_text=None, application_json=None):
		self.http = http or 'HTTP/1.1'
		self.status_code = status_code
		self.reason_phrase = reason_phrase or 'OK'
		self.header = header or {}
		self.body = body

		if plain_text is not None:
			self.header['Content-Type'] = 'plain/text'
			self.body = plain_text.encode('utf-8')
		elif application_json is not None:
			self.header['Content-Type'] = 'application/json'
			self.body = application_json.encode('utf-8')

HTTP_RESPONSE_SCHEMA = {
	'http': ar.Unicode(),
	'status_code': ar.Integer8(),
	'reason_phrase': ar.Unicode(),
	'header': ar.MapOf(ar.Unicode(),ar.Unicode()),
	'body': ar.Block(),
}

ar.bind(HttpRequest, object_schema=HTTP_REQUEST_SCHEMA)
ar.bind(HttpResponse, object_schema=HTTP_RESPONSE_SCHEMA)

# Conversion of messages to on-the-wire blocks, and back again.
# Sync HTTP request-response, fully typed (HTTP body).
# Accepted patterns.
REQUEST_LINE = -1
EMPTY_LINE = -2
HEADER_LINE = -3
BODY_BLOCK = -4
DISCARD_LINE = -5
RESPONSE_LINE = -6

# Character sets and constants for FSM.
HTTP_TEXT = set(ord(c) for c in string.digits) | set(ord(c) for c in string.ascii_letters) | set(ord(c) for c in string.punctuation)
HTTP_SPACE = set(ord(c) for c in [' ', '\t'])
HTTP_DIGIT = set(ord(c) for c in string.digits)
HTTP_PRINTING = HTTP_TEXT | HTTP_SPACE

CR = 13		# Carriage return.
NL = 10		# Form feed.
CN = 58		# Colon.

# Limits for elements of HTTP.
METHOD_LENGTH = 10
REQUEST_LENGTH = 8000
HTTP_LENGTH = 16
NAME_LENGTH = 128
VALUE_LENGTH = 1024
PENDING_REQUESTS = 256

SERVER_SLASH_VERSION = 'Ansar-API-server/1.0'
CLIENT_SLASH_VERSION = 'Ansar-API-client/1.0'

LARGE_BODY = 256 * 1024

# Auto conversion of a member in an object to
# a representation in a form.

D2S = {
	ar.Boolean:		lambda d: 'true' if d else 'false',
	ar.Character:	lambda d: d,
	ar.Rune:		lambda d: d,
	ar.Integer2:	lambda d: str(d),
	ar.Integer4:	lambda d: str(d),
	ar.Integer8:	lambda d: str(d),
	ar.Byte:		lambda d: str(d),
	ar.Unsigned2:	lambda d: str(d),
	ar.Unsigned4:	lambda d: str(d),
	ar.Unsigned8:	lambda d: str(d),
	ar.Float4:		lambda d: str(d),
	ar.Float8:		lambda d: str(d),
	ar.String:		lambda d: d,
	ar.Unicode:		lambda d: d,
	ar.ClockTime:	lambda d: ar.clock_to_text(d),
	ar.WorldTime:	lambda d: ar.world_to_text(d),
	ar.TimeSpan:	lambda d: ar.span_to_text(d),
	ar.TimeDelta:	lambda d: ar.delta_to_text(d),
	ar.UUID:		lambda d: str(d),
}

def stream_form(k, v, d):
	c = D2S.get(type(v), None)
	if c:
		return c(d)
	return None

# Auto conversion of a representation in a form to
# a member in an object.

S2D = {
	ar.Boolean:		lambda s: s == 'true',
	ar.Character:	lambda s: s,
	ar.Rune:		lambda s: s,
	ar.Integer2:	lambda s: int(s),
	ar.Integer4:	lambda s: int(s),
	ar.Integer8:	lambda s: int(s),
	ar.Byte:		lambda s: int(s),
	ar.Unsigned2:	lambda s: int(s),
	ar.Unsigned4:	lambda s: int(s),
	ar.Unsigned8:	lambda s: int(s),
	ar.Float4:		lambda s: float(s),
	ar.Float8:		lambda s: float(s),
	ar.String:		lambda s: s,
	ar.Unicode:		lambda s: s,
	ar.ClockTime:	lambda s: ar.text_to_clock(s),
	ar.WorldTime:	lambda s: ar.text_to_world(s),
	ar.TimeSpan:	lambda s: ar.text_to_span(s),
	ar.TimeDelta:	lambda s: ar.text_to_delta(s),
	ar.UUID:		lambda s: uuid.UUID(s),
	ar.Type:		lambda s: ar.decode_type(s),
}

def recover_form(k, v, d):
	c = S2D.get(type(v), None)
	if c:
		return c(d)
	return None

# Stream the representation of an HTTP request onto the
# byte sequence provided.
def stream_request(encoded_bytes, method=None, request_uri=None, http=None, header=None, body=None):
	method = method or 'POST'
	header = header or {}
	http = http or 'HTTP/1.1'

	if body:
		content_length = len(body)
		request = (f'{method} {request_uri} {http}\r\n'
			f'User-Agent: {CLIENT_SLASH_VERSION}\r\n')

	else:
		content_length = 0
		request = (f'{method} {request_uri} {http}\r\n'
			f'User-Agent: {CLIENT_SLASH_VERSION}\r\n')

	if 'Content-Length' not in header:
		request += f'Content-Length: {content_length}\r\n'

	if 'Content-Type' not in header:
		request += 'Content-Type: application/x-www-form-urlencoded\r\n'

	if header:
		kv = [f'{k}: {v}\r\n' for k, v in header.items()]
		kv = ''.join(kv)
		request += kv
	request += '\r\n'

	encoded_bytes += request.encode('utf-8')
	if body:
		encoded_bytes += body

# Stream the representation of an HTTP response onto the
# byte sequence provided.
def stream_response(encoded_bytes, http=None, status_code=200, reason_phrase='OK', header=None, body=None):
	http = http or 'HTTP/1.1'
	header = header or {}
	now = ar.world_now()
	date = now.strftime('%a, %d %b %Y %H:%M:%S GMT')

	if body:
		content_length = len(body)
		response = (f'{http} {status_code} {reason_phrase}\r\n'
			f'Date: {date}\r\n'
			f'Server: {SERVER_SLASH_VERSION}\r\n'
			f'Content-Length: {content_length}\r\n')
	else:
		response = (f'{http} {status_code} {reason_phrase}\r\n'
			f'Date: {date}\r\n'
			f'Server: {SERVER_SLASH_VERSION}\r\n'
			f'Content-Length: 0\r\n')

	if header:
		kv = [f'{k}: {v}\r\n' for k, v in header.items()]
		kv = ''.join(kv)
		response += kv
	response += '\r\n'

	encoded_bytes += response.encode('utf-8')
	if body:
		encoded_bytes += body

# Inbound requests and outbound responses.
class ApiServerStream(object):
	def __init__(self, transport):
		self.transport = transport

		# Specific to input decoding.
		self.analysis_state = 1
		self.method = bytearray()
		self.request = bytearray()
		self.http = bytearray()

		# Lexical FSMs - REQUEST_LINE.
		def s1(c):
			if c in HTTP_TEXT:
				self.method.append(c)		# GET/POST/...
				return 2
			elif c == CR:
				return 11
			elif c == NL:
				return DISCARD_LINE
			return 10

		def s2(c):
			if c in HTTP_TEXT:
				n = len(self.method)
				if n < METHOD_LENGTH:
					self.method.append(c)	# GET/POST/... (cont)
					return 2
				return 10
			elif c in HTTP_SPACE:
				return 3
			elif c == CR:
				return 11
			elif c == NL:
				return DISCARD_LINE
			return 10

		def s3(c):
			if c in HTTP_SPACE:
				return 3
			elif c in HTTP_TEXT:
				self.request.append(c)		# Request URI.
				return 4
			elif c == CR:
				return 11
			elif c == NL:
				return DISCARD_LINE
			return 10

		def s4(c):
			if c in HTTP_TEXT:
				n = len(self.request)
				if n < REQUEST_LENGTH:
					self.request.append(c)	# Request URI (cont).
					return 4
				return 10
			elif c in HTTP_SPACE:
				return 5
			elif c == CR:
				return 11
			elif c == NL:
				return DISCARD_LINE
			return 10

		def s5(c):
			if c in HTTP_SPACE:
				return 5
			elif c in HTTP_TEXT:
				self.http.append(c)			# HTTP version.
				return 6
			elif c == CR:
				return 11
			elif c == NL:
				return DISCARD_LINE
			return 10

		def s6(c):
			if c in HTTP_TEXT:
				n = len(self.http)
				if n < HTTP_LENGTH:
					self.http.append(c)		# HTTP version (cont).
					return 6
				return 10
			elif c in HTTP_SPACE:
				return 9
			elif c == CR:
				return 8
			elif c == NL:
				return REQUEST_LINE			# Shortcut request line.
			return 10

		def s8(c):
			if c == NL:
				return REQUEST_LINE			# Request line.
			return 10

		def s9(c):
			if c in HTTP_SPACE:
				return 9
			elif c == CR:
				return 8
			elif c == NL:
				return REQUEST_LINE			# Shortcut request line.
			return 10

		# Failed the proper layout, discard up to next line termination.
		def s10(c):
			if c == CR:
				return 11
			elif c == NL:
				return DISCARD_LINE
			return 10

		def s11(c):
			if c == NL:
				return DISCARD_LINE
			return 10

		# HEADER_LINE.
		def s20(c):
			if c in HTTP_TEXT:
				self.name.append(c)		# Header name.
				return 21
			elif c in HTTP_SPACE:
				return 40
			elif c == CR:
				return 41
			elif c == NL:
				return EMPTY_LINE
			return 10

		def s21(c):
			if c == CN:					# Colon. MUST check this first.
				return 22
			elif c in HTTP_TEXT:
				n = len(self.name)
				if n < NAME_LENGTH:
					self.name.append(c)	# Header name (cont).
					return 21
				return 10
			elif c == CR:
				return 11
			elif c == NL:
				return DISCARD_LINE
			return 10

		def s22(c):
			if c in HTTP_SPACE:		# Between colon and value.
				return 23
			elif c == CR:
				return 11
			elif c == NL:
				return DISCARD_LINE
			return 10

		def s23(c):
			if c in HTTP_PRINTING:
				self.value.append(c)	# Header value.
				return 24
			elif c == CR:
				return 11
			elif c == NL:
				return DISCARD_LINE
			return 10

		def s24(c):
			if c in HTTP_PRINTING:
				n = len(self.value)
				if n < VALUE_LENGTH:
					self.value.append(c)	# Header value (cont).
					return 24
				return 10
			elif c in HTTP_SPACE:
				return 27
			elif c == CR:
				return 26
			elif c == NL:
				return HEADER_LINE
			return 10

		def s26(c):
			if c == NL:
				return HEADER_LINE
			return 10

		def s27(c):
			if c in HTTP_SPACE:
				return 27
			elif c == CR:
				return 26
			elif c == NL:
				return HEADER_LINE
			return 10

		# EMPTY_LINE
		def s40(c):
			if c in HTTP_SPACE:
				return 40
			elif c == CR:
				return 41
			elif c == NL:
				return EMPTY_LINE
			return 10

		def s41(c):
			if c == NL:
				return EMPTY_LINE
			return 10

		# BODY_BLOCK
		def s30(c):
			self.body.append(c)
			self.length -= 1
			if self.length < 1:
				return BODY_BLOCK
			return 30

		self.shift = {
			1: s1, 2: s2, 3: s3, 4: s4, 5: s5,
			6: s6, 8: s8, 9: s9, 10: s10, 11: s11,

			20: s20, 21: s21, 22: s22, 23: s23,
			24: s24, 26: s26, 27: s27,

			40: s40, 41: s41,

			30: s30,
		}

	# Message being sent from HTTP server.
	# Push the optimal representation of the next message onto the byte stream.
	def	message_to_block(self, mtr):
		m, t, r = mtr
		transport = self.transport
		encoded_bytes = transport.encoded_bytes
		codec = transport.codec
		ansar_client = transport.parent.request.ansar_client

		# App is sending an explicit HTTP response.
		# Add the barest automation, e.g. content length.
		if isinstance(m, HttpResponse):
			stream_response(encoded_bytes,
				http=m.http, status_code=m.status_code, reason_phrase=m.reason_phrase,
				header=m.header, body=m.body)
			return

		# Ansar at remote end?
		if ansar_client:
			header = {'Content-Type': 'application/json'}
			e = codec.encode(m, ar.Any())
			body = e.encode('utf-8')

			stream_response(encoded_bytes,
				header=header,
				body=body)
			return

		# Not a full integration. May as well provide full
		# details even if the client doesnt understand it.
		# Good for command-line scenarios, e.g. curl.
		header = {'Content-Type': 'application/json'}
		if isinstance(m, ar.Faulted):
			sr = (500, 'Server Error')
			fs = str(m)
		else:
			sr = (200, 'OK')
		codec.pretty_format = True
		e = codec.encode(m, ar.Any())
		body = e.encode('utf-8')

		stream_response(encoded_bytes,
			status_code=sr[0], reason_phrase=sr[1],
			header=header,
			body=body)

	# Complete zero or more messages, using the given block.
	def recover_message(self, received, sockets):
		for c in received:
			next = self.shift[self.analysis_state](c)
			if next > 0:
				self.analysis_state = next
				continue

			# Poor man's reduce-goto.
			if next == REQUEST_LINE:
				self.analysis_state = 20
				self.name = bytearray()
				self.value = bytearray()
				self.header = {}
				self.body = None
				self.length = 0
				continue
			elif next == HEADER_LINE:
				name = self.name.decode('ascii')
				value = self.value.decode('ascii')
				self.header[name] = value
				self.analysis_state = 20
				self.name = bytearray()
				self.value = bytearray()
				continue
			elif next == EMPTY_LINE:
				s = self.header.get('Content-Length', None)
				if s:
					self.length = int(s)
					if self.length > LARGE_BODY:
						# Discard everything. Should close. Likely
						# to land in nowheres-ville and fail anyway.
						self.analysis_state = 1
						self.method = bytearray()
						self.request = bytearray()
						self.http = bytearray()
						continue
					self.body = bytearray()
					if self.length > 0:
						self.analysis_state = 30
						continue
			elif next == BODY_BLOCK:
				pass
			else:
				# Discard line,
				self.analysis_state = 1
				self.method = bytearray()
				self.request = bytearray()
				self.http = bytearray()
				continue

			body, to_address, return_address = self.recover_hyper_text()

			# Completed frame.
			yield body, to_address, return_address

			# Restart.
			self.analysis_state = 1
			self.method = bytearray()
			self.request = bytearray()
			self.http = bytearray()

	# An HTTP request has been received by an HTTP server.
	# Recover the best-possible message and forward to receiver.
	def recover_hyper_text(self):
		transport = self.transport
		upgrade = transport.upgrade
		codec = transport.codec
		return_address = transport.return_proxy
		to_address = transport.local_termination

		# Parsed elements of the HTTP request
		method = self.method.decode('ascii')
		request = self.request.decode('ascii')
		http = self.http.decode('ascii')
		content_type = self.header.get('Content-Type', None)
		body = self.body

		# Handling details.
		api_server = self.transport.parent.request.api_server
		default_to_request = self.transport.parent.request.default_to_request

		def lookup(name):
			for a in api_server:
				if name == a.__art__.name:
					return a
			return None

		try:	# Raise exception for immediate error response.

			if not request.startswith('/') or len(request) < 2:
				raise ValueError(f'unexpected request URI "{request}"')

			slash = request.rfind('/')			# Test above guarantees a slash.
			name = request[slash + 1:]			# Only want the trailing name.
			question = name.find('?')
			if question != -1:
				query = name[question+1:]
				name = name[:question]
			else:
				query = ''

			tom = lookup(name)
			if not tom:
				if not default_to_request:
					raise ValueError(f'unknown and defaulting to HTTP requests disabled')
			elif question != -1:
				schema = tom.__art__.value
				message = tom()
				a = query.split('&')
				for kv in a:
					if not kv:
						continue
					equals = kv.find('=')
					if equals < 0:
						raise ValueError(f'unexpected field "{kv}" query')
					k = kv[:equals]
					v = kv[equals + 1:]
					q = unquote_plus(v)

					s = schema.get(k, None)
					if s is None:
						raise ValueError(f'unknown key "{k}" in "{name}" query')

					d = recover_form(k, s, q)
					if d is None:
						raise ValueError(f'no conversion for "{k}" ({q})')
					setattr(message, k, d)

				return message, to_address, return_address

			elif content_type == 'application/x-www-form-urlencoded':
				schema = tom.__art__.value
				message = tom()
				if body:
					# Breakout the flat form into a key-value dict.
					d = body.decode('utf-8')
					a = d.split('&')

					for kv in a:
						equals = kv.find('=')
						if equals < 0:
							raise ValueError(f'unexpected field "{kv}" x-www-form')
						k = kv[:equals]
						v = kv[equals + 1:]
						q = unquote_plus(v)

						s = schema.get(k, None)
						if s is None:
							raise ValueError(f'unknown key "{k}" in "{name}" x-www-form')

						d = recover_form(k, s, q)
						if d is None:
							raise ValueError(f'no conversion for "{k}" ({q})')
						setattr(message, k, d)

				return message, to_address, return_address

			elif content_type == 'application/json':
				# Unlikely that 3rd party JSON will be in ansar layout.
				# This is effectively for ansar clients.
				if method != 'POST':
					raise ValueError(f'unexpected HTTP method "{method}"')
				if body:
					json = body.decode('utf-8')
					try:
						message, v = codec.decode(json, ar.UserDefined(tom))
					except (ar.CodecFailed, OverflowError, ValueError) as e:
						s = str(e)
						raise ValueError(s)

					if v is not None:
						if not upgrade:
							raise ValueError(f'body with version "{v}" and no upgrade available')
						message = upgrade(message, v)
				else:
					message = tom()
				return message, to_address, return_address
			else:
				# Matched in the API but not a content that
				# can be processed
				raise ValueError(f'unexpected content type "{content_type}" for HTTP request "{name}"')

		except (ValueError, ar.ConversionError) as e:
			s = str(e)
			s = f'cannot recover request "{request}" ({s})'
			message = HttpResponse(status_code=400, reason_phrase='Bad Request',
					plain_text=s)
			return message, return_address, to_address

		# Default is to pass on any request that parsed correctly
		# but did not meet semantic criteria.
		message = HttpRequest(method=method, request_uri=request, http=http,
			header=self.header,
			body=self.body)
		return message, to_address, return_address

#
#
class INITIAL: pass
class READY: pass

class ApiClientSession(ar.Point, ar.StateMachine):
	def __init__(self, remote_address=None, **kv):
		ar.Point.__init__(self)
		ar.StateMachine.__init__(self, INITIAL)
		self.remote_address = remote_address
		self.pending = ar.deque()

def ApiClientSession_INITIAL_Start(self, message):
	return READY

def ApiClientSession_READY_Unknown(self, message):
	# A message passing through, either a response from the
	# remote end or another request from a local object.
	if self.return_address[-1] == self.remote_address[-1]:	# Response from remote.
		if self.pending:									# Yes there is a matching request.
			m, r = self.pending.popleft()
			self.send(message, r)							# Send response to original client.
			if self.pending:								# A waiting request?
				m, r = self.pending[0]
				self.send(m, self.remote_address)
		else:
			t = ar.tof(message)
			self.warning(f'message "{t}" from HTTP server has no matching request')
	elif len(self.pending) > PENDING_REQUESTS:
		self.reply(HttpResponse(status_code=400, reason_phrase='Client Error', body=b'Request queue overflow'))
	else:
		mr = (message, self.return_address)				# Request from local client.
		self.pending.append(mr)							# Remember.
		if len(self.pending) == 1:						# Nothing pending.
			self.send(message, self.remote_address)
	return READY

def ApiClientSession_READY_Stop(self, message):
	self.complete(ar.Aborted())

API_CLIENT_SESSION_DISPATCH = {
	INITIAL: (
		(ar.Start,), ()
	),
	READY: (
		(ar.Unknown, ar.Stop), ()
	),
}

ar.bind(ApiClientSession, API_CLIENT_SESSION_DISPATCH, thread='api-client-session')

# 
#
class ApiClientStream(object):
	def __init__(self, transport):
		self.transport = transport

		# Specific to input decoding.
		self.analysis_state = 1

		# Response.
		self.http = bytearray()
		self.code = bytearray()
		self.reason = bytearray()
		# Header.
		self.name = bytearray()
		self.value = bytearray()
		self.header = {}
		# Body.
		self.body = None

		# RESPONSE_LINE.
		def s1(c):
			if c in HTTP_TEXT:
				self.http.append(c)		# HTTP/...
				return 2
			elif c == CR:
				return 11
			elif c == NL:
				return DISCARD_LINE
			return 10

		def s2(c):
			if c in HTTP_TEXT:
				n = len(self.http)
				if n < METHOD_LENGTH:
					self.http.append(c)	# HTTP/...
					return 2
				return 10
			elif c in HTTP_SPACE:
				return 3
			elif c == CR:
				return 11
			elif c == NL:
				return DISCARD_LINE
			return 10

		def s3(c):
			if c in HTTP_SPACE:
				return 3
			elif c in HTTP_DIGIT:
				self.code.append(c)		# Response code.
				return 4
			elif c == CR:
				return 11
			elif c == NL:
				return DISCARD_LINE
			return 10

		def s4(c):
			if c in HTTP_DIGIT:
				n = len(self.code)
				if n < 8:
					self.code.append(c)	# Response code.
					return 4
				return 10
			elif c in HTTP_SPACE:
				return 5
			elif c == CR:
				return 11
			elif c == NL:
				return DISCARD_LINE
			return 10

		def s5(c):
			if c in HTTP_SPACE:
				return 5
			elif c in HTTP_PRINTING:
				self.reason.append(c)		# Reason phrase.
				return 7
			elif c == CR:
				return 8
			elif c == NL:
				return DISCARD_LINE
			return 10

		def s7(c):
			if c in HTTP_PRINTING:
				n = len(self.reason)
				if n < HTTP_LENGTH:
					self.reason.append(c)	# Reason phrase.
					return 7
				return 10
			elif c == CR:
				return 8
			elif c == NL:
				return RESPONSE_LINE			# Shortcut response line.
			return 10

		def s8(c):
			if c == NL:
				return RESPONSE_LINE			# Response line.
			return 10

		# Failed the proper layout, discard up to next line termination.
		def s10(c):
			if c == CR:
				return 11
			elif c == NL:
				return DISCARD_LINE
			return 10

		def s11(c):
			if c == NL:
				return DISCARD_LINE
			return 10

		# HEADER_LINE.
		def s20(c):
			if c in HTTP_TEXT:
				self.name.append(c)		# Header name.
				return 21
			elif c in HTTP_SPACE:
				return 40
			elif c == CR:
				return 41
			elif c == NL:
				return EMPTY_LINE
			return 10

		def s21(c):
			if c == CN:		# Colon. Check this first.
				return 22
			elif c in HTTP_TEXT:
				n = len(self.name)
				if n < NAME_LENGTH:
					self.name.append(c)	# Header name.
					return 21
				return 10
			elif c == CR:
				return 11
			elif c == NL:
				return DISCARD_LINE
			return 10

		def s22(c):
			if c in HTTP_SPACE:		# Between colon and value.
				return 23
			elif c == CR:
				return 11
			elif c == NL:
				return DISCARD_LINE
			return 10

		def s23(c):
			if c in HTTP_PRINTING:
				self.value.append(c)	# Header value.
				return 24
			elif c == CR:
				return 11
			elif c == NL:
				return DISCARD_LINE
			return 10

		def s24(c):
			if c in HTTP_PRINTING:
				n = len(self.value)
				if n < VALUE_LENGTH:
					self.value.append(c)	# Header value.
					return 24
				return 10
			elif c in HTTP_SPACE:
				return 27
			elif c == CR:
				return 26
			elif c == NL:
				return HEADER_LINE
			return 10

		def s26(c):
			if c == NL:
				return HEADER_LINE
			return 10

		def s27(c):
			if c in HTTP_SPACE:
				return 27
			elif c == CR:
				return 26
			elif c == NL:
				return HEADER_LINE
			return 10

		# EMPTY_LINE
		def s40(c):
			if c in HTTP_SPACE:
				return 40
			elif c == CR:
				return 41
			elif c == NL:
				return EMPTY_LINE
			return 10

		def s41(c):
			if c == NL:
				return EMPTY_LINE
			return 10

		# BODY_BLOCK
		def s30(c):
			self.body.append(c)
			self.length -= 1
			if self.length < 1:
				return BODY_BLOCK
			return 30

		self.shift = {
			1: s1, 2: s2, 3: s3, 4: s4, 5: s5,
			7: s7, 8: s8,

			10: s10, 11: s11,

			20: s20, 21: s21, 22: s22, 23: s23,
			24: s24, 26: s26, 27: s27,

			40: s40, 41: s41,

			30: s30,
		}

	# Message being sent from HTTP client.
	# Push the optimal representation of the next message onto the byte stream.
	def	message_to_block(self, mtr):
		m, t, r = mtr
		transport = self.transport
		codec = transport.codec
		encoded_bytes = transport.encoded_bytes
		api_client = transport.parent.request.api_client
		ansar_server = transport.parent.request.ansar_server
		tom = type(m)
		art = tom.__art__

		# Client is sending an explicit HTTP request.
		# Add the barest automation, e.g. prepend path, content type and length.
		if isinstance(m, HttpRequest):
			request_uri = f'{api_client}{m.request_uri}'
			stream_request(encoded_bytes,
				method=m.method, request_uri=request_uri, http=m.http,
				header=m.header, body=m.body)
			return

		if ansar_server:
			content_type = 'application/json'
			e = codec.encode(m, ar.UserDefined(tom))
			body = e.encode('utf-8')
		else:
			content_type = 'application/x-www-form-urlencoded'
			schema = tom.__art__.value
			ks = []
			for k, v in schema.items():
				# Form representation of null is to omit.
				d = getattr(m, k, None)
				if d is None:
					continue
				# Apply auto conversions.
				s = stream_form(k, v, d)
				if s is None:
					raise ValueError(f'cannot stream "{k}" x-www-form')
				q = quote_plus(s)
				ks.append(f'{k}={q}')

			if ks:
				form = '&'.join(ks)
				body = form.encode('utf-8')
			else:
				body = None

		request_uri = f'{api_client}{art.name}'
		stream_request(encoded_bytes,
			method='POST', request_uri=request_uri,
			header={'Content-Type': content_type},
			body=body)

	# Complete zero or more messages, consuming the given block.
	def recover_message(self, received, sockets):
		for c in received:
			next = self.shift[self.analysis_state](c)
			if next > 0:
				self.analysis_state = next
				continue

			# Poor man's reduce-goto.
			if next == RESPONSE_LINE:
				self.analysis_state = 20
				self.name = bytearray()
				self.value = bytearray()
				self.header = {}
				self.body = None
				self.length = 0
				continue
			elif next == HEADER_LINE:
				name = self.name.decode('ascii')
				value = self.value.decode('ascii')
				self.header[name] = value
				self.analysis_state = 20
				self.name = bytearray()
				self.value = bytearray()
				continue
			elif next == EMPTY_LINE:
				s = self.header.get('Content-Length', None)
				if s:
					self.length = int(s)
					self.body = bytearray()
					if self.length > 0:
						self.analysis_state = 30
						continue
			elif next == BODY_BLOCK:
				pass
			else:
				# Discard line,
				self.analysis_state = 1
				self.http = bytearray()
				self.code = bytearray()
				self.reason = bytearray()
				continue

			body, to_address, return_address = self.recover_hyper_text()

			# Completed frame.
			yield body, to_address, return_address

			# Restart.
			self.analysis_state = 1
			self.http = bytearray()
			self.code = bytearray()
			self.reason = bytearray()

	# HTTP response has been received by this client.
	# Generate the optimal message and forward to receiver.
	def recover_hyper_text(self):
		transport = self.transport
		codec = transport.codec
		upgrade = transport.upgrade

		return_address = transport.return_proxy
		to_address = transport.local_termination

		ansar_server = transport.parent.request.ansar_server

		http = self.http.decode('ascii')
		code = self.code.decode('ascii')
		code = int(code)
		reason = self.reason.decode('ascii')
		content_type = self.header.get('Content-Type', None)
		body = self.body

		if not ansar_server:
			# Compile elements into local message and send.
			message = HttpResponse(http=http, status_code=code, reason_phrase=reason,
				header=self.header,
				body=self.body)
			return message, to_address, return_address

		# Solely for when client is connected to ansar server.
		# Preconditions for json decode.
		client_error = None
		if http != 'HTTP/1.1':
			client_error = f'unexpected HTTP version "{http}"'
		elif content_type != 'application/json':
			message = HttpResponse(http=http, status_code=code, reason_phrase=reason,
				header=self.header,
				body=self.body)
			return message, to_address, return_address
		elif not body or not isinstance(body, bytearray):
			client_error = f'no JSON body'
		else:
			v = None
			try:
				body = body.decode('utf-8')
				message, v = codec.decode(body, ar.Any())
			except (ar.CodecFailed, OverflowError, ValueError) as e:
				s = str(e)
				client_error = f'unexpected body "{s}"'

		if client_error:
			message = HttpResponse(http=http, status_code=500, reason_phrase='Server Error',
				header={'Content-Type': 'plain/text'},
				body=client_error.encode('utf-8'))
			return message, to_address, return_address

		if v is not None:
			if not upgrade:
				client_error = f'HTTP response "{code}/{reason}" with body version "{v}" and no upgrade'
				message = HttpResponse(status_code=500, reason_phrase='Server Error',
					header={'Content-Type': 'plain/text'},
					body=client_error.encode('utf-8'))
				return message, to_address, return_address
			message = upgrade(message, v)
		return message, to_address, return_address
