import asynchat, asyncore
import re, os, sys
import ssl
import socket
import time
import zlib
from warnings import warn
from errno import ECONNRESET, ENOTCONN, ESHUTDOWN, ECONNABORTED, EWOULDBLOCK
import select
import threading
from . import adns
from aquests.protocols.http2 import H2_PROTOCOLS

from aquests.lib.ssl_ import resolve_cert_reqs, resolve_ssl_version, create_urllib3_context

NPN_PROTOCOL = 'h2'
H2_NPN_PROTOCOLS = [NPN_PROTOCOL, 'h2-16', 'h2-15', 'h2-14']
DEBUG = False
	
class SocketPanic (Exception): pass
class TimeOut (Exception): pass

class AsynConnect (asynchat.async_chat):
	ac_in_buffer_size = 65535
	ac_out_buffer_size = 65535
	zombie_timeout = 10
	keep_alive_timeout = 10
	network_delay_timeout = 10	
	ready = None
	affluent = None	
	
	request_count = 0
	active = 0
	proxy = False

	def __init__ (self, address, lock = None, logger = None):
		self.address = address
		self.lock = lock
		self.logger = logger
		self._cv = threading.Condition ()		
		self.__sendlock = None
		self.__no_more_request = False
		self.set_event_time ()
		self.proxy = False
		self.proxy_client = False
		self.handler = None
		self.initialize_connection ()
		asynchat.async_chat.__init__ (self)
	
	def close (self):
		if self._closed:
			return
		
		if self.socket:
			# self.socket is still None, when DNS not found
			asynchat.async_chat.close (self)
		
		# re-init asychat
		self.ac_in_buffer = b''
		self.incoming = []
		self.producer_fifo.clear()
		
		if self.handler and self.errcode:		
			self.handler.connection_closed (self.errcode, self.errmsg)
		
		if not self.proxy_client:
			self.logger ("[info] .....asyncon %s:%d has been closed" % self.address)

		self.set_proto (None)
		self.set_active (False)
		self.handler = None
		self._closed = True
			
	def end_tran (self):
		self.del_channel ()
		self.handler = None
		self.set_active (False)
				
	def use_sendlock (self):
		self.__sendlock = threading.Lock ()
		self.initiate_send = self._initiate_send_ts
		
	def _initiate_send_ts (self):
		with self.__sendlock:
			return asynchat.async_chat.initiate_send (self)
				
	def get_proto (self):
		with self.lock:
			p = self._proto
		return p
	
	def set_proto (self, proto):
		with self.lock:
			self._proto = proto		
				
	def get_history (self):
		return self.__history
				
	def initialize_connection (self):		
		self._closed = False
		self._raised_ENOTCONN = 0 # for win32
		self.__history = []
		self._proto = None
		self._handshaking = False
		self._handshaked = False		
		
		self.established = False		
		self.upgraded = False		
		self.ready = None
		self.affluent = None
		
	def set_event_time (self):
		self.event_time = time.time ()
	
	def is_proxy (self):
		return self.proxy
			
	def log (self, msg, logtype):
		if self.handler is not None and hasattr (self.handler, "log"):
			self.handler.log (msg, logtype)
		elif self.logger:
			self.logger (msg, logtype)
		else:
			warn ("No logger")
			
	def trace (self):
		if self.handler is not None and hasattr (self.handler, "trace"):
			self.handler.trace ()
		elif self.logger:
			self.logger.trace ()
		else:
			warn ("No logger for traceback")
				
	def duplicate (self):
		return self.__class__ (self.address, self.lock, self.logger)
		
	def clean_shutdown_control (self, phase, time_in_this_phase):	
		self.__no_more_request = True
		if self.isactive () or (self.handler and self.handler.working ()):
			return 1
		else:				
			self.handle_close ()
			return 0
		
	def readable (self):
		if self.affluent is not None:
			return asynchat.async_chat.readable (self)	and self.affluent ()
		return asynchat.async_chat.readable (self)
	
	def writable (self):
		if self.ready is not None:
			return asynchat.async_chat.writable (self) and self.ready ()
		return asynchat.async_chat.writable (self)	
  
	def is_channel_in_map (self, map = None):
		if map is None:
			map = self._map
		return self._fileno in map
		
	def set_active (self, flag, nolock = False):
		if flag:
			flag = time.time ()
		else:
			flag = 0
			
		if nolock or self.lock is None:
			self.active = flag
			return
			
		self.lock.acquire ()
		self.active = flag
		self.request_count += 1
		self.lock.release ()
	
	def get_active (self, nolock = False):
		if nolock or self.lock is None:
			return self.active
		self.lock.acquire ()
		active = self.active
		self.lock.release ()	
		return active
	
	def isactive (self):	
		return self.get_active () > 0
	
	def isconnected (self):
		with self.lock:
			r = self.connected
		return r	
		
	def get_request_count (self):	
		return self.request_count
	
	def add_channel (self, map = None):		
		self.zombie_timeout =  self.network_delay_timeout
		self._fileno = self.socket.fileno ()
		return asynchat.async_chat.add_channel (self, map)		
		
	def del_channel (self, map = None):
		asynchat.async_chat.del_channel (self, map)
		# make default and sometimes reset server'stimeout	
		self.zombie_timeout =  self.keep_alive_timeout
				
	def create_socket (self, family, type):
		self.family_and_type = family, type
		sock = socket.socket (family, type)
		sock.setblocking (0)
		self.set_socket (sock)
	
	def connect (self):
		if adns.query:
			adns.query (self.address [0], "A", callback = self.continue_connect)
		else:
			# no adns query
			self.continue_connect (True)
		
	def continue_connect (self, answer = None):
		if not answer:
			self.log ("DNS not found - %s" % self.address [0], "error")
			return self.handle_close (704, "DNS Not Found")
		
		self.initialize_connection ()
		self.create_socket (socket.AF_INET, socket.SOCK_STREAM)
		
		try:
			if not adns.query:				
				asynchat.async_chat.connect (self, self.address)
				
			else:	
				res = adns.get (self.address [0], "A")	
				if res:
					ip = res [-1]["data"]
					if ip:
						asynchat.async_chat.connect (self, (ip, self.address [1]))																		
				else:
					self.handle_close (704, "DNS Not Found")
					
		except:	
			self.handle_error ()
	
	def recv (self, buffer_size):		
		self.set_event_time ()
		try:
			data = self.socket.recv(buffer_size)	
			#print ("+++++DATA", len (data), repr (data [:40]))
			if not data:
				self.handle_close (700, "Connection closed unexpectedly in recv")
				return b''
			else:
				return data		
		except socket.error as why:
			if why.errno in asyncore._DISCONNECTED:				
				self.handle_close (700, "Connection closed unexpectedly in recv")
				return b''				
			else:
				raise
	
	def send (self, data):		
		#print ("SEND", data)
		self.set_event_time ()
		try:
			return self.socket.send (data)
		except socket.error as why:
			if why.errno == EWOULDBLOCK:
				return 0				
			elif why.errno in asyncore._DISCONNECTED:
				#print (">>>>>>>>>> why.errno == asyncore.ENOTCONN", why.errno == asyncore.ENOTCONN)
				if os.name == "nt" and why.errno == asyncore.ENOTCONN:
					# winsock sometimes raise ENOTCONN and sometimes recovered.
					# Found this error at http://file.hungryboarder.com:8080/HBAdManager/pa.html?siteId=hboarder&zoneId=S-2
					if self._raised_ENOTCONN <= 3:
						self._raised_ENOTCONN += 1
						return 0
					else:
						self._raised_ENOTCONN = 0
				self.handle_close (700, "Connection closed unexpectedly in send")
				return 0
			else:
				raise
	
	def close_if_over_keep_live (self):
		if time.time () - self.event_time > self.zombie_timeout:
			self.disconnect ()
	
	def set_zombie_timeout (self, timeout = 10):
		self.zombie_timeout = timeout
	
	def set_zombie_timeout_by_case (self):
		if self.affluent or self.ready:
			self.zombie_timeout = self.network_delay_timeout * 2
		else:	
			self.zombie_timeout = self.network_delay_timeout
	
	def set_timeout (self, a = 10, b = 10):
		self.set_keep_alive_timeout (a)
		self.set_network_delay_timeout (b)
		
	def set_network_delay_timeout (self, timeout = 10):
		self.network_delay_timeout = timeout
	
	def set_keep_alive_timeout (self, timeout = 10):
		self.keep_alive_timeout = timeout
		
	def handle_connect (self):
		if hasattr (self.handler, "has_been_connected"):		
			self.handler.has_been_connected ()
		
	def handle_read (self):
		self.set_zombie_timeout_by_case ()		
		asynchat.async_chat.handle_read (self)
		
	def handle_write (self):
		self.set_zombie_timeout_by_case ()		
		asynchat.async_chat.handle_write (self)
			
	def handle_expt (self):
		self.logger ("socket panic", "fail")
		self.handle_close (703, "Socket Panic")
	
	def handle_error (self):
		self.trace ()
		self.handle_close(701, "Exception")
	
	def handle_timeout (self):
		self.log ("socket timeout", "fail")
		self.handle_close (702, "Zombie Timeout")
		
	def handle_expt_event(self):
		err = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR)
		if err != 0:
			self.log ("Socket Error %d Occurred" % err, "warn")
			self.handle_close (706, "Socket %d Error" % err)
		else:
			self.handle_expt ()
	
	def maintern (self, object_timeout):
		if time.time () - self.event_time > object_timeout:
			if self.handler and hasattr (self.handler, "enter_shutdown_process"):
				self.handler.enter_shutdown ()
				return True
			else:	
				self.disconnect ()
				return True		
		return False
		
	# proxy POST need no init_send
	def push (self, thing, init_send = True):
		if type (thing) is bytes:
			asynchat.async_chat.push (self, thing)
		else:
			self.push_with_producer (thing, init_send)	
		
	def push_with_producer (self, producer, init_send = True):
		self.producer_fifo.append (producer)
		if init_send:
			self.initiate_send ()
	
	def handle_abort (self):
		self.handler = None
		self.close ()
		
	def handle_close (self, code = 700, msg = "Disconnected by Server"):
		self.errcode = code
		self.errmsg = msg
		self.close ()
							
	def collect_incoming_data (self, data):
		if not self.handler:
			self.logger ("recv data but no hander, droping data %d" % len (data), "warn")
			self.disconnect ()
			return
		self.handler.collect_incoming_data (data)
	
	def found_terminator (self):
		if not self.handler:
			self.logger ("found terminator but no handler", "warn")
			self.disconnect ()
			return # already closed
		self.handler.found_terminator ()
	
	def disconnect (self):
		# no error
		self.handle_close (0, "")
	
	def reconnect (self):
		self.disconnect ()
		self.connect ()
	
	def set_proxy (self, flag = True):
		self.proxy = flag
		
	def set_proxy_client (self, flag = True):
		self.proxy_client = flag
						
	def begin_tran (self, handler):
		if self.__no_more_request:
			raise SystemError ("Entered Shutdown Process")
		
		self.errcode = 0
		self.errmsg = ""
		
		self.handler = handler
		self.set_event_time ()
		self.proxy_client = False
		
		if self.connected:
			self.close_if_over_keep_live () # check keep-alive
		
		# IMP: call add_channel () AFTER push()	otherwise threading issue will be raised
		try:
			if self.connected:
				#should keep order but it seems meaningless? LOOK LATER with initiate_send
				#self.initiate_send ()
				self.add_channel ()
			else:
				self.connect ()
		except:
			self.handle_error ()


class AsynSSLConnect (AsynConnect):	
	ac_out_buffer_size = 65536
	ac_in_buffer_size = 65536
		
	def handshake (self):
		if not self._handshaking:
			err = self.socket.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR)
			if err != 0:
				raise socket.error(err, _strerror(err))								
			ssl_context = create_urllib3_context(ssl_version=resolve_ssl_version(None), cert_reqs=resolve_cert_reqs(None))
			
			try: ssl_context.set_alpn_protocols (H2_PROTOCOLS)
			except AttributeError: ssl_context.set_npn_protocols (H2_PROTOCOLS)								
			self.socket = ssl_context.wrap_socket (self.socket, do_handshake_on_connect = False, server_hostname = self.address [0])
			self._handshaking = True
			
		try:
			self.socket.do_handshake ()
		except ssl.SSLError as why:
			if why.args [0] in (ssl.SSL_ERROR_WANT_READ, ssl.SSL_ERROR_WANT_WRITE):
				return False
			raise ssl.SSLError(why)
		
		try: self._proto = self.socket.selected_alpn_protocol()
		except (AttributeError, NotImplementedError): 
			try: self._proto = self.socket.selected_npn_protocol()
			except (AttributeError, NotImplementedError): pass

		self._handshaked = True		
		return True
							
	def handle_connect_event (self):	
		if not self._handshaked and not self.handshake ():
			return
		# handshaking done
		self.handle_connect()
		self.connected = True
		
	def recv (self, buffer_size):
		self.set_event_time ()
		try:
			data = self.socket.recv (buffer_size)			
			if not data:				
				self.handle_close (700, "Connection closed unexpectedly")
				return b''
			else:				
				return data

		except ssl.SSLError as why:
			if why.errno == ssl.SSL_ERROR_WANT_READ:
				try: 
					raise BlockingIOError				
				except NameError:
					raise socket.error (EWOULDBLOCK)
													
			# closed connection
			elif why.errno in (ssl.SSL_ERROR_ZERO_RETURN, ssl.SSL_ERROR_EOF):
				self.handle_close (700, "Connection closed by SSL_ERROR_ZERO_RETURN or SSL_ERROR_EOF")
				return b''
				
			else:
				raise

	def send (self, data):
		self.set_event_time ()
		try:
			return self.socket.send (data)			

		except ssl.SSLError as why:
			if why.errno == ssl.SSL_ERROR_WANT_WRITE:
				return 0
			elif why.errno == ssl.SSL_ERROR_ZERO_RETURN:				
				self.handle_close (700, "Connection closed by SSL_ERROR_ZERO_RETURN")
				return 0
			else:
				raise


class AsynSSLProxyConnect (AsynSSLConnect, AsynConnect):
	ac_out_buffer_size = 65536
	ac_in_buffer_size = 65536
	
	def handle_connect_event (self):
		if self.established:
			AsynSSLConnect.handle_connect_event (self)
		else:	
			AsynConnect.handle_connect_event (self)
	
	def recv (self, buffer_size):		
		if self._handshaked or self._handshaking:
			return AsynSSLConnect.recv (self, buffer_size)				
		else:
			return AsynConnect.recv (self, buffer_size)
			
	def send (self, data):		
		if self._handshaked or self._handshaking:
			return AsynSSLConnect.send (self, data)
		else:
			return AsynConnect.send (self, data)

	