# Author: Scott Woods <scott.suzuki@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 re
from urllib.parse import quote_plus, unquote_plus
from enum import Enum

from .virtual_memory import *
from .convert_memory import *
from .convert_signature import *
from .convert_type import *
from .message_memory import *
from .bind_type import *
from .command_startup import *
from .virtual_codec import *
from .json_codec import *
from .noop_codec import *
from .virtual_point import *
from .point_runtime import *
from .point_machine import *

__all__ = [
	'HttpRequest',
	'HttpResponse',
	'FormRequest',
	'HttpMethod',
	'decode_resource',
	'decode_body',
	'cannot_be_dispatched',
	'cannot_be_converted',
	'ResourceDispatch',
	'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, ...
	:param request_uri: the path section of a URI
	:param http: version of HTTP
	:param header: name-value pairs
	:param body: payload, value
	"""
	def __init__(self, method: str=None, request_uri: str=None, http: str=None, header: dict[str,str]=None, body: bytearray=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

# 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
	:param status_code: 100, 200, 300, ...
	:param reason_phrase: short description
	:param header: name-value pairs
	:param body: payload, value
	"""
	def __init__(self, http: str=None, status_code: int=200, reason_phrase: str=None,
			header: dict[str,str]=None, body: bytearray=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

bind(HttpRequest)
bind(HttpResponse)

# Inbound to form server.
class FormRequest(object):
	"""The message generated by an HTTP client and received by an HTTP server.

	Passing an :class:`~.ReForm` to :func:`~.listen` causes special processing
	of the request URI. The URI is deconstructed into a FormRequest and presented
	to the calling object, e.g. a server. This should be combined with use of
	the :class:`~.ResourceDispatch`, to route inbound FormRequests to a resource-
	based function.

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

bind(FormRequest)

#
#
class HttpMethod(Enum):
	CONNECT=1
	DELETE=2
	GET=3
	HEAD=4
	OPTIONS=5
	PATCH=6
	POST=7
	PUT=8
	TRACE=9

def decode_resource(body, portable):
	if body is None:
		return None

	codec = CodecJson()
	json = body.decode('utf-8')
	try:
		message = codec.decode(json, portable)
	except (CodecError, OverflowError, ValueError) as e:
		s = str(e)
		raise ValueError(s)
	return message

def decode_body(portable):
	if hasattr(portable, '__art__'):
		portable = UserDefined(portable)

	def decode(body, ignored):
		if body is None:
			return None
		codec = CodecJson()
		json = body.decode('utf-8')
		try:
			message = codec.decode(json, portable)
		except (CodecError, OverflowError, ValueError) as e:
			s = str(e)
			raise ValueError(s)
		return message
	return decode

def cannot_be_dispatched(self, request=None, error=None):
	cannot = f'Cannot map request [{request.method}] "{request.request}" ({error})'
	self.warning(cannot, **request.form_entry)
	return HttpResponse(status_code=400, reason_phrase='Bad Request', body=cannot)

def cannot_be_converted(self, request=None, error=None):
	c = ','.join([f'{k}={v}' for k, v in request.form_entry.items()])
	cannot = f'Cannot complete conversion "{c}" ({error})'
	self.warning(cannot)
	return HttpResponse(status_code=400, reason_phrase='Bad Request', body=cannot)

class ResourceDispatch(object):
	'''Build up the info needed to dispatch from FormRequest to function.

	Define a global instance passing the ReForm and list of resource types.	Accept
	mappings of request-to-function, using the :meth:`~.ResourceDispatch.add` method.
	Then expect calls to :meth:`~.ResourceDispatch.lookup` that matches the runtime FormRequest to a saved
	function.
	'''
	def __init__(self, form: ReForm, resource: list[Type]):
		entry_names = form.entry_names()
		if 'resource' not in entry_names:
			raise ValueError('form has no "resource" entry')
		entry_names.remove('resource')
		self.entry_names = entry_names
		self.resource_class = {r.__name__.lower(): r for r in resource}
		self.call_frame = {}

	def add(self, resource: Type, method: HttpMethod, f, **kv):
		'''Add a mapping for a resource+method to a function.

		Compose a key based on resource, method and the set of
		expected args (``kv``). Then ``table[key] = f``. Each ``kv``
		entry is a name and a conversion function.
		'''
		expected = tuple(s in kv for s in self.entry_names)
		key = (resource, method, *expected)
		self.call_frame[key] = (f, kv)

	def lookup(self, request: FormRequest):
		'''Resolve the function to call based on info in FormRequest.

		Compose a key based on resource, method and the set of
		detected args (``request.form_entry``), convert the args from
		matched text to application values, Return function and args.
		'''
		try:
			resource = self.resource_class[request.form_entry['resource']]
			method = HttpMethod[request.method]
			expected = tuple(request.form_entry[s] is not None for s in self.entry_names)
			key = (resource, method, *expected)
			call = self.call_frame.get(key, None)
			if call is None:
				f = cannot_be_dispatched
				a = {
					'request': request,
					'error': 'no matching call signature',
				}
				return f, a
		except (ValueError, IndexError, KeyError) as e:
			s = str(e)
			f = cannot_be_dispatched
			a = {
				'request': request,
				'error': 'unusable resource, method or URI',
			}
			return f, a

		try:
			f, args = call
			a = {}
			for k, c in args.items():
				if k == 'header':
					value = request.header if c is None else c(request.header)
				elif k == 'http':
					value = request.http if c is None else c(request.http)
				elif k == 'body':
					value = request.body if c is None else c(request.body, UserDefined(resource))
				else:
					entry = request.form_entry[k]
					value = entry if c is None else c(entry)
				a[k] = value
		except (ValueError, IndexError, KeyError, TypeError) as e:
			s = str(e)
			f = cannot_be_converted
			a = {
				'request': request,
				'error': s,
			}
		return f, a

	def __call__(self, layer, request: FormRequest):
		'''Resolve the request to a function and call it.

		Make the call to lookup() and execute the resulting call materials.
		If the result of that call is not a None - send it on to the
		original client.
		'''
		return_address = layer.return_address
		resource_function, entry_args = self.lookup(request)
		response = resource_function(layer, **entry_args)
		if response is not None:
			layer.send(response, return_address)

# 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 = 'kipjak-API-server/1.0'
CLIENT_SLASH_VERSION = 'kipjak-API-client/1.0'
SERVER_REST_VERSION = 'kipjak-REST-server/1.0'

LARGE_BODY = 256 * 1024

# 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=None, header=None, body=None, restful=False):
	http = http or 'HTTP/1.1'
	header = header or {}
	now = world_now()
	date = now.strftime('%a, %d %b %Y %H:%M:%S GMT')

	if reason_phrase is None:
		status_code, reason_phrase = faulted_status(status_code)

	server_version = SERVER_SLASH_VERSION if not restful else SERVER_REST_VERSION
	if body:
		content_length = len(body)
		response = (f'{http} {status_code} {reason_phrase}\r\n'
			f'Date: {date}\r\n'
			f'Server: {server_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_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

def faulted_status(error_code):
	status_code = error_code or 500
	if status_code < 100:
		status_code = 500
		reason = 'Server Error'
	elif status_code < 200:
		reason = 'Information'
	elif status_code < 300:
		reason = 'OK'
	elif status_code < 400:
		reason = 'Multiple Choices'
	elif status_code < 500:
		reason = 'Client Error'
	elif status_code < 600:
		reason = 'Server Error'
	else:
		status_code = 500
		reason = 'Server Error'
	return status_code, reason

# 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
		search_subs = self.transport.parent.search_subs
		restful = search_subs is not None

		content_json = {'Content-Type': 'application/json'}
		content_text = {'Content-Type': 'plain/text'}
		if isinstance(m, Faulted):
			status_code, reason_phrase = faulted_status(m.error_code)
			e = codec.encode(m, Any())
			body = e.encode('utf-8')
			stream_response(encoded_bytes,
				status_code=status_code, reason_phrase=reason_phrase,
				header=content_json,
				body=body, restful=restful)
		elif not isinstance(m, HttpResponse):
			e = codec.encode(m, Any())
			body = e.encode('utf-8')
			stream_response(encoded_bytes,
				status_code=200, reason_phrase='OK',
				header=content_json,
				body=body, restful=restful)
		elif m.body is not None:
			# Special case handling; allow sending of text and
			# specific content type and payload.
			if isinstance(m.body, bytearray):
				if 'Content-Type' not in m.header:
					body = f'Custom response but "Content-Type" not specified'.encode('utf-8')
					stream_response(encoded_bytes, status_code=500, reason_phrase='Server Error',
						header=content_text, body=body, restful=restful)
					return
				stream_response(encoded_bytes,
					http=m.http, status_code=m.status_code, reason_phrase=m.reason_phrase,
					header=m.header, body=m.body, restful=restful)
				return
			elif isinstance(m.body, str):
				m.header['Content-Type'] = 'plain/text'
				body = m.body.encode('utf-8')
				stream_response(encoded_bytes,
					http=m.http, status_code=m.status_code, reason_phrase=m.reason_phrase,
					header=m.header, body=body, restful=restful)
				return

			# Standard processing. The body is an instance of Any,
			# i.e. either a class instance or a constructed type.
			m.header['Content-Type'] = 'application/json'
			e = codec.encode(m.body, Any())
			body = e.encode('utf-8')
			stream_response(encoded_bytes,
				http=m.http, status_code=m.status_code, reason_phrase=m.reason_phrase,
				header=m.header, body=body, restful=restful)
		else:
			stream_response(encoded_bytes,
				http=m.http, status_code=m.status_code, reason_phrase=m.reason_phrase,
				header=m.header, restful=restful)

	# 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
		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')
		header = self.header
		content_type = self.header.get('Content-Type', None)
		body = self.body

		# Handling details.
		named_type = self.transport.parent.named_type
		search_subs = self.transport.parent.search_subs
		default_to_request = self.transport.parent.request.default_to_request

		try:	# Raise exception for immediate error response.

			if search_subs:
				search, subs = search_subs
				match = search.fullmatch(request)
				if match:
					form_entry = {s: match[s] for s in subs}
					message = FormRequest(method, request, form_entry, http, header, body)
					return message, to_address, return_address
				raise ValueError(f'request URI does not match form')

			elif 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 = named_type.get(name, None)
			if tom is None:
				if not default_to_request:
					raise ValueError(f'unknown and defaulting to HTTP requests disabled')
			elif question != -1:
				schema = tom.__art__.schema
				message = tom()
				a = query.split('&')
				try:
					c = CodecJson()
					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)

						t = schema.get(k, None)
						if t is None:
							raise ValueError(f'unknown key "{k}" in "{name}" query')
						d = decode_argument(c, q, t)
						setattr(message, k, d)
				except CodecError as e:
					s = str(e)
					raise ValueError(f'no conversion for "{k}" ({s})')
				return message, to_address, return_address
			elif content_type == 'application/x-www-form-urlencoded':
				schema = tom.__art__.schema
				message = tom()
				if body:
					# Breakout the flat form into a key-value dict.
					d = body.decode('utf-8')
					a = d.split('&')

					try:
						c = CodecJson()
						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)

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

							d = decode_argument(c, q, t)
							setattr(message, k, d)
					except CodecError as e:
						s = str(e)
						raise ValueError(f'no conversion for "{k}" ({s})')

				return message, to_address, return_address

			elif content_type == 'application/json':
				# Unlikely that 3rd party JSON will be in kipjak layout.
				# This is effectively for kipjak clients.
				if method != 'POST':
					raise ValueError(f'unexpected HTTP method "{method}"')
				if body:
					json = body.decode('utf-8')
					try:
						message = codec.decode(json, UserDefined(tom))
					except (CodecError, OverflowError, ValueError) as e:
						s = str(e)
						raise ValueError(s)
				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, ConversionError) as e:
			s = str(e)
			s = f'cannot recover request "{request}" ({s})'
			message = HttpResponse(status_code=400, reason_phrase='Bad Request', body=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(Point, StateMachine):
	def __init__(self, proxy_address=None, **kv):
		Point.__init__(self)
		StateMachine.__init__(self, INITIAL)
		self.proxy_address = proxy_address
		self.pending = 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.proxy_address[-1]:	# Response from remote.
		if self.pending:									# Yes there is a matching request.
			m, t, r = self.pending.popleft()
			c = cast_to(message, self.received_type)
			self.send(c, r)									# Send response to original client.
			if self.pending:								# A waiting request?
				m, t, r = self.pending[0]
				c = cast_to(m, t)
				self.send(m, self.proxy_address)
		else:
			#t = tof(message)
			self.warning(f'message "tof" from HTTP server has no matching request')
	elif len(self.pending) > PENDING_REQUESTS:
		self.reply(HttpResponse(status_code=400, reason_phrase='Client Error', body='Request queue overflow'))
	else:
		mtr = (message, self.received_type, self.return_address)				# Request from local client.
		self.pending.append(mtr)							# Remember.
		if len(self.pending) == 1:						# Nothing pending.
			self.send(message, self.proxy_address)
	return READY

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

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

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
		http_client = transport.parent.request.http_client
		application_json = transport.parent.request.application_json
		tom = type(m)
		art = getattr(tom, '__art__', None)
		if art is None:
			raise ValueError('HTTP client output limited to class instances (including HttpResponse)')

		# 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'{http_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 application_json:
			content_type = 'application/json'
			e = codec.encode(m, UserDefined(tom))
			body = e.encode('utf-8')
		else:
			content_type = 'application/x-www-form-urlencoded'
			schema = tom.__art__.schema
			ks = []
			try:
				c = CodecNoop()
				for k, t in schema.items():
					# Form representation of null is to omit.
					d = getattr(m, k, None)
					if d is None:
						continue
					s = encode_argument(c, d, t)
					q = quote_plus(s)
					ks.append(f'{k}={q}')
			except (CodecError, ValueError) as e:
				raise ValueError(f'cannot stream "{art.path}" as x-www-form, {e}')

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

		request_uri = f'{http_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

		return_address = transport.return_proxy
		to_address = transport.local_termination

		application_json = transport.parent.request.application_json

		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 application_json:
			# 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 kipjak 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 = codec.decode(body, Any())
			except (CodecError, 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', body=client_error)
			return message, to_address, return_address

		return message, to_address, return_address
