#
# arcus-python-client - Arcus python client drvier
# Copyright 2014 NAVER Corp.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
 

import sys
import socket
import time
import os
import re
import threading
from threading import Lock
import select

from arcus import *


# Some parts of Connection and ArcusMCNode is came from python memcache module
class Connection(object):
	def __init__(self, host):
		ip, port = host.split(':')
		self.ip = ip
		self.port = int(port)
		self.address = ( self.ip, self.port )

		self.socket = None
		self.buffer = b''

		self.connect()

	def connect(self):
		if self.socket:
			disconnect()

		self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
		try:
			self.socket.connect(self.address)
		except socket.timeout as msg:
			self.disconnect()
		except socket.error as msg:
			self.disconnect()

		self.buffer = b''
		return self.socket

	def disconnect(self):
		if self.socket:
			self.socket.close()
			self.socket = None

	def disconnected(self):
		return self.socket == None

	def send_request(self, request):
		arcuslog(self, 'send_request: ', request + b'\r\n')
		self.socket.sendall(request + b'\r\n')

	def hasline(self):
		index = self.buffer.find(b'\r\n')
		return index >= 0

	def readline(self):
		buf = self.buffer

		while True:
			index = buf.find(b'\r\n')
			if index >= 0:
				break

			data = self.socket.recv(4096)
			arcuslog(self, 'sock recv: (%d): "' % len(data), data)

			if data == None:
				self.disconnect()
				raise ArcusNodeConnectionException('connection lost')

			buf += data

		self.buffer = buf[index+2:]

		arcuslog(self, 'readline: ', buf[:index])
		return buf[:index]

	def recv(self, rlen):
		buf = self.buffer
		while len(buf) < rlen:
			foo = self.socket.recv(max(rlen - len(buf), 4096))

			if foo == None:
				raise ArcusNodeSocketException('Read %d bytes, expecting %d, read returned 0 length bytes' % ( len(buf), rlen ))

			buf += foo
			arcuslog(self, 'sock recv: (%d): ' % len(foo), foo)

		self.buffer = buf[rlen:]
		arcuslog(self, 'recv: ', buf[:rlen])
		return buf[:rlen]


class ArcusMCNode:
	worker = None
	shutdown = False

	def __init__(self, addr, name, transcoder, node_allocator):
		#mandatory files
		self.addr = addr
		self.name = name
		self.in_use = False
		self.transcoder = transcoder

		self.handle = Connection(addr)
		self.ops = []
		self.lock = Lock() # for ordering worker.q and ops

		self.node_allocator = node_allocator

	def __repr__(self):
		return '%s-%s' % (self.addr, self.name)

	def get_fileno(self):
		return self.handle.socket.fileno()

	def disconnect(self):
		# disconnect socket
		self.handle.disconnect()

		# clear existing operation
		for op in self.ops:
			op.set_invalid()

		self.ops = []
		
	def disconnect_all(self): # shutdown
		self.node_allocator.shutdown = True
		self.disconnect()

		self.node_allocator.worker.q.put(None)
		
	def process_request(self, request):
		if self.handle.disconnected():
			ret = self.handle.connect()
			if ret != None:
				# re-register if node connection is available
				self.node_allocator.worker.register_node(self)

		self.handle.send_request(request)



	##########################################################################################
	### commands
	##########################################################################################
	def get(self, key):
		return self._get('get', key)

	def gets(self, key):
		return self._get('gets', key)

	def set(self, key, val, exptime=0):
		return self._set("set", key, val, exptime)

	def cas(self, key, val, cas_id, exptime=0):
		return self._cas(key, 'cas', val, cas_id, exptime)

	def incr(self, key, value=1):
		return self._incr_decr("incr", key, value)

	def decr(self, key, value=1):
		return self._incr_decr("decr", key, value)

	def add(self, key, val, exptime=0):
		return self._set("add", key, val, exptime)

	def append(self, key, val, exptime=0):
		return self._set("append", key, val, exptime)

	def prepend(self, key, val, exptime=0):
		return self._set("prepend", key, val, exptime)

	def replace(self, key, val, exptime=0):
		return self._set("replace", key, val, exptime)

	def delete(self, key):
		full_cmd = "delete %s" % key
		return self.add_op('delete', bytes(full_cmd, 'utf-8'), self._recv_delete)

	def flush_all(self):
		full_cmd = b'flush_all'
		return self.add_op('flush_all', full_cmd, self._recv_ok)

	def get_stats(self, stat_args = None):
		if stat_args == None:
			full_cmd = b'stats'
		else:
			full_cmd = bytes('stats ' + stat_args, 'utf-8')

		op = self.add_op('stats', full_cmd, self._recv_stat)

	def lop_create(self, key, flags, exptime=0, noreply=False, attr=None):
		return self._coll_create('lop create', key, flags, exptime, noreply, attr)

	def lop_insert(self, key, index, value, noreply=False, pipe=False, attr=None):
		return self._coll_set('lop insert', key, index, value, noreply, pipe, attr)

	def lop_delete(self, key, range, drop=False, noreply=False, pipe=False):
		option = ''
		if drop == True:
			option += 'drop'

		if noreply == True:
			option += ' noreply'

		if pipe == True:
			assert noreply == False
			option += ' pipe'

		if isinstance(range, tuple):
			full_cmd = bytes('lop delete %s %d..%d %s' % (key, range[0], range[1], option), 'utf-8');
			return self.add_op('lop delete', full_cmd, self._recv_delete, noreply or pipe)
		else:
			full_cmd = bytes('lop delete %s %d %s' % (key, range, option), 'utf-8');
			return self.add_op('lop delete', full_cmd, self._recv_delete, noreply or pipe)


	def lop_get(self, key, range, delete=False, drop=False):
		return self._coll_get('lop get', key, range, self._recv_lop_get, delete, drop)

	def sop_create(self, key, flags, exptime=0, noreply=False, attr=None):
		return self._coll_create('sop create', key, flags, exptime, noreply, attr)

	def sop_insert(self, key, value, noreply=False, pipe=False, attr=None):
		return self._coll_set('sop insert', key, None, value, noreply, pipe, attr)

	def sop_get(self, key, count=0, delete=False, drop=False):
		return self._coll_get('sop get', key, count, self._recv_sop_get, delete, drop)

	def sop_delete(self, key, val, drop=False, noreply=False, pipe=False):
		flags, len, value = self.transcoder.encode(val)

		option = '%d' % len
		if drop == True:
			option += 'drop'

		if noreply == True:
			option += ' noreply'

		if pipe == True:
			assert noreply == False
			option += ' pipe'

		option += '\r\n'

		full_cmd = bytes('sop delete %s %s' % (key, option), 'utf-8') + value;
		return self.add_op('sop delete', full_cmd, self._recv_delete, noreply or pipe)

	def sop_exist(self, key, val, pipe=False):
		flags, len, value = self.transcoder.encode(val)

		option = '%d' % len
		if pipe == True:
			assert noreply == False
			option += ' pipe'

		option += '\r\n'

		full_cmd = bytes('sop exist %s %s' % (key, option), 'utf-8') + value
		return self.add_op('sop exist', full_cmd, self._recv_exist, pipe)


	def bop_create(self, key, flags, exptime=0, noreply=False, attr=None):
		return self._coll_create('bop create', key, flags, exptime, noreply, attr)

	def bop_insert(self, key, bkey, value, eflag=None, noreply=False, pipe=False, attr=None):
		return self._coll_set('bop insert', key, None, value, noreply, pipe, attr, bkey, eflag)

	def bop_upsert(self, key, bkey, value, eflag=None, noreply=False, pipe=False, attr=None):
		return self._coll_set('bop upsert', key, None, value, noreply, pipe, attr, bkey, eflag)

	def bop_update(self, key, bkey, value, eflag=None, noreply=False, pipe=False, attr=None):
		return self._coll_set('bop update', key, None, value, noreply, pipe, attr, bkey, eflag)

	def bop_delete(self, key, range, filter=None, count=None, drop=False, noreply=False, pipe=False):
		option = ''

		if filter != None:
			option += filter.get_expr() + ' '

		if count != None:
			option += '%d ' % count

		if drop == True:
			option += 'drop'

		if noreply == True:
			option += ' noreply'

		if pipe == True:
			assert noreply == False
			option += ' pipe'

		if isinstance(range, tuple):
			if isinstance(range[0], str):
				if range[0][:2] != '0x' or range[1][:2] != '0x':
					raise CollectionHexFormat()

				full_cmd = bytes('bop delete %s %s..%s %s' % (key, range[0], range[1], option), 'utf-8')
				return self.add_op('bop delete', full_cmd, self._recv_delete, noreply or pipe)
			else:
				full_cmd = bytes('bop delete %s %d..%d %s' % (key, range[0], range[1], option), 'utf-8')
				return self.add_op('bop delete', full_cmd, self._recv_delete, noreply or pipe)
		else:
			if isinstance(range, str):
				if range[:2] != '0x':
					raise CollectionHexFormat()

				full_cmd = bytes('bop delete %s %s %s' % (key, range, option), 'utf-8')
				return self.add_op('bop delete', full_cmd, self._recv_delete, noreply or pipe)
			else:
				full_cmd = bytes('bop delete %s %d %s' % (key, range, option), 'utf-8')
				return self.add_op('bop delete', full_cmd, self._recv_delete, noreply or pipe)

	def bop_get(self, key, range, filter=None, delete=False, drop=False):
		return self._coll_get('bop get', key, range, self._recv_bop_get, delete, drop, filter=filter)

	def bop_mget(self, key_list, range, filter=None, offset=None, count=50):
		return self._coll_mget('bop mget', key_list, range, filter, offset, count)

	def bop_smget(self, key_list, range, filter=None, offset=None, count=2000):
		return self._coll_mget('bop smget', key_list, range, filter, offset, count)

	def bop_count(self, key, range, filter):
		return self._coll_get('bop count', key, range, self._recv_bop_get, filter=filter)

	def bop_incr(self, key, bkey, value, noreply=False, pipe=False):
		return self._bop_incrdecr('bop incr', key, bkey, value, noreply, pipe)
		
	def bop_decr(self, key, bkey, value, noreply=False, pipe=False):
		return self._bop_incrdecr('bop decr', key, bkey, value, noreply, pipe)


	##########################################################################################
	### Queue senders
	##########################################################################################
	def add_op(self, cmd, full_cmd, callback, noreply = False):
		op = ArcusOperation(self, full_cmd, callback)
		arcuslog(self, 'add operation %s(%s:%s) to %s' % (full_cmd, callback, hex(id(op)), self))

			
		if noreply: # or pipe
			# don't need to receive response, set_result now
			self.node_allocator.worker.q.put(op)
			op.set_result(True)
		else:
			self.lock.acquire()
			self.node_allocator.worker.q.put(op)
			self.ops.append(op)
			self.lock.release()

		return op

	def _get(self, cmd, key):
		full_cmd = bytes("%s %s" % (cmd, key), 'utf-8')
		if cmd == 'gets':
			callback = self._recv_cas_value
		else:
			callback = self._recv_value

		op = self.add_op(cmd, full_cmd, callback)
		return op

	def _set(self, cmd, key, val, exptime=0):
		flags, len, value = self.transcoder.encode(val)
		if flags == None:
			return(0)

		full_cmd = bytes("%s %s %d %d %d\r\n" % (cmd, key, flags, exptime, len), 'utf-8')
		full_cmd += value

		op = self.add_op(cmd, full_cmd, self._recv_set)
		return op 

	def _cas(self, cmd, key, val, cas_id, exptime=0):
		flags, len, value = self.transcoder.encode(val)
		if flags == None:
			return(0)

		full_cmd = bytes("%s %s %d %d %d %d\r\n" % (cmd, key, flags, exptime, len, cas_id), 'utf-8')
		full_cmd += value

		op = self.add_op(cmd, full_cmd, self._recv_set)
		return op 

	def _incr_decr(self, cmd, key, value):
		full_cmd = "%s %s %d" % (cmd, key, value)

		op = self.add_op(cmd, bytes(full_cmd, 'utf-8'), self._recv_set)
		return op

	def _coll_create(self, cmd, key, flags, exptime=0, noreply=False, attr=None):
		if attr == None:
			attr = {}

		# default value
		if 'maxcount' not in attr:
			attr['maxcount'] = 4000
		if 'ovflaction' not in attr:
			attr['ovflaction'] = 'tail_trim'
		if 'readable' not in attr:
			attr['readable'] = True

		option = '%d %d %d' % (flags, exptime, attr['maxcount'])
		if attr['ovflaction'] != 'tail_trim':
			option += ' ' + attr['ovflaction']
		if attr['readable'] == False:
			option += ' unreadable'

		if noreply == True:
			option += ' noreply'

		full_cmd = bytes('%s %s %s' % (cmd, key, option), 'utf-8')
		return self.add_op(cmd, full_cmd, self._recv_coll_create, noreply)

	def _bop_incrdecr(self, cmd, key, bkey, val, noreply=False, pipe=False):
		if isinstance(val, int):
			value = '%d' % val
		else:
			value = val

		if isinstance(bkey, int):
			bkey_str = '%d' % bkey
		else:
			if bkey[:2] != '0x':
				raise CollectionHexFormat()
			bkey_str = '%s' % bkey

		option = '%s %s' % (bkey_str, value)
	
		if noreply == True:
			option += ' noreply'

		if pipe == True:
			assert noreply == False
			option += ' pipe'

		full_cmd = bytes('%s %s %s' % (cmd, key, option), 'utf-8')
		return self.add_op(cmd, full_cmd, self._recv_set, noreply or pipe)

	def _coll_set(self, cmd, key, index, val, noreply=False, pipe=False, attr=None, bkey=None, eflag=None):
		flags, len, value = self.transcoder.encode(val)

		if bkey != None:	# bop
			assert index == None

			if isinstance(bkey, int):
				bkey_str = '%d' % bkey
			else:
				if bkey[:2] != '0x':
					raise CollectionHexFormat()
				bkey_str = '%s' % bkey

			if eflag != None:
				if eflag[:2] != '0x':
					raise CollectionHexFormat()
				option = '%s %s %d' % (bkey_str, eflag, len)
			else:
				option = '%s %d' % (bkey_str, len)
		elif index != None:	# lop
			option = '%d %d' % (index, len)
		else:			# sop
			option = '%d' % (len)
	
		if attr != None:
			# default mandatory value
			if 'flags' not in attr:
				attr['flags'] = 0
			if 'exptime' not in attr:
				attr['exptime'] = 0
			if 'maxcount' not in attr:
				attr['maxcount'] = 4000

			option += ' create %d %d %d' % (attr['flags'], attr['exptime'], attr['maxcount'])
			if 'ovflaction' in attr:
				option += ' ' + attr['ovflaction']
			if 'readable' in attr and attr['readable'] == False:
				option += ' unreadable'

		if noreply == True:
			option += ' noreply'

		if pipe == True:
			assert noreply == False
			option += ' pipe'

		option += '\r\n'

		full_cmd = bytes('%s %s %s' % (cmd, key, option), 'utf-8') + value
		return self.add_op(cmd, full_cmd, self._recv_coll_set, noreply or pipe)

	def _coll_get(self, cmd, key, range, callback, delete=None, drop=None, filter=None):
		option = ''
		type = cmd[:3]

		if filter != None:
			option += filter.get_expr() + ' '

		if delete == True:
			option += 'delete'

		if drop == True:
			assert delete == False
			option += 'drop'

		if isinstance(range, tuple):
			if type == 'bop' and isinstance(range[0], str):
				if range[0][:2] != '0x' or range[1][:2] != '0x':
					raise CollectionHexFormat()

				full_cmd = bytes("%s %s %s..%s %s" % (cmd, key, range[0], range[1], option), 'utf-8')
				return self.add_op(cmd, full_cmd, callback);
			else:
				full_cmd = bytes("%s %s %d..%d %s" % (cmd, key, range[0], range[1], option), 'utf-8')
				return self.add_op(cmd, full_cmd, callback);
		else:
			if type == 'bop' and isinstance(range, str):
				if range[:2] != '0x':
					raise CollectionHexFormat()

				full_cmd = bytes("%s %s %s %s" % (cmd, key, range, option), 'utf-8')
				return self.add_op(cmd, full_cmd, callback);
			else:
				full_cmd = bytes("%s %s %d %s" % (cmd, key, range, option), 'utf-8')
				return self.add_op(cmd, full_cmd, callback);


	def _coll_mget(self, org_cmd, key_list, range, filter, offset, count):

		comma_sep_keys = ''
		for key in key_list:
			if comma_sep_keys != '':
				comma_sep_keys += ','
			comma_sep_keys += key

		cmd = '%s %d %d ' % (org_cmd, len(comma_sep_keys), len(key_list))

		if isinstance(range, tuple):
			if isinstance(range[0], str):
				if range[0][:2] != '0x' or range[1][:2] != '0x':
					raise CollectionHexFormat()

				cmd += '%s..%s' % range
			else:
				cmd += '%d..%d' % range
		else:
			if isinstance(range, str):
				if range[:2] != '0x':
					raise CollectionHexFormat()

				cmd += '%s' % range
			else:
				cmd += '%d' % range

		if filter != None:
			cmd += ' ' + filter.get_expr()

		if offset != None:
			cmd += ' %d' % offset
		cmd += ' %d' % count

		cmd += '\r\n%s' % comma_sep_keys
		cmd = bytes(cmd, 'utf-8')

		if org_cmd == 'bop mget':
			reply = self._recv_mget
		else:
			reply = self._recv_smget
		
		op = self.add_op(org_cmd, cmd, reply)

		return op 

	

	##########################################################################################
	### recievers
	##########################################################################################
	def do_op(self):
		self.lock.acquire()
		if len(self.ops) <= 0:
			arcuslog('ops empty (%s)' % self.addr)
			self.lock.release()
			return

		op = self.ops.pop(0)
		self.lock.release()

		try:
			ret = op.callback()
		except Exception as e:
			arcuslog('do op failed: %s' % str(e))
			ret = e

		op.set_result(ret)

		while self.handle.hasline(): # remaining jobs
			self.lock.acquire()
			op = self.ops.pop(0)
			self.lock.release()

			try:
				ret = op.callback()
			except Exception as e:
				arcuslog('do op failed: %s' % str(e))
				ret = e

			op.set_result(ret)

	def _recv_ok(self):
		line = self.handle.readline()
		if line == b'OK':
			return True

		return False

	def _recv_stat(self):
		data = {}
		while True:
			line = handle.readline()
			if line[:3] == b'END' or line is None:
				break

			dummy, k, v = line.split(' ', 2)
			data[k] = v

			return data

	def _recv_set(self):
		line = self.handle.readline()
		if line[0:8] == b'RESPONSE':
			dummy, count = line.split()

			ret = []
			for i in range(0, int(count)):
				line = self.handle.readline()
				ret.append(line.decode('utf-8'))

			line = self.handle.readline() # b'END'
		
			return ret
				
			
		if line == b'STORED':
			return True

		if line == b'NOT_FOUND':
			return False

		if line == b'TYPE_MISMATCH':
			raise CollectionType()

		if line == b'OVERFLOWED':
			raise CollectionOverflow() 

		if line == b'OUT_OF_RANGE':
			raise CollectionIndex()

		if line.isdigit():	# incr, decr, bop incr, bop decr
			return int(line)

		return False

	def _recv_delete(self):
		line = self.handle.readline()
		if line[0:8] == b'RESPONSE':
			dummy, count = line.split()

			ret = []
			for i in range(0, int(count)):
				line = self.handle.readline()
				ret.append(line.decode('utf-8'))

			line = self.handle.readline() # b'END'
		
			return ret
				
		if line == b'DELETED':
			return True

		if line == b'NOT_FOUND':
			return True # True ?? (or exception)

		if line == b'TYPE_MISMATCH':
			raise CollectionType()

		if line == b'OVERFLOWED':
			raise CollectionOverflow() 

		if line == b'OUT_OF_RANGE' or line == b'NOT_FOUND_ELEMENT':
			raise CollectionIndex()

		return False

	def _recv_cas_value(self): 
		line = self.handle.readline()
		if line[:5] != b'VALUE':
			return None

		resp, rkey, flags, len, cas_id = line.split()
		flags = int(flags)
		rlen = int(len)
		val = self._decode_value(flags, rlen)
		return (val, cas_id)

	def _recv_value(self):
		line = self.handle.readline()
		if line[:5] != b'VALUE':
			return None

		resp, rkey, flags, len = line.split()
		flags = int(flags)
		rlen = int(len)
		return self._decode_value(flags, rlen)

	def _recv_coll_create(self):
		line = self.handle.readline()
		if line == b'CREATED':
			return True

		if line == b'EXISTS':
			raise CollectionExist()

		return False

	def _recv_coll_set(self):
		line = self.handle.readline()
		if line[0:8] == b'RESPONSE':
			dummy, count = line.split()

			ret = []
			for i in range(0, int(count)):
				line = self.handle.readline()
				ret.append(line.decode('utf-8'))

			line = self.handle.readline() # b'END'
		
			return ret
				
		if line == b'STORED':
			return True

		if line == b'NOT_FOUND':
			return False

		if line == b'TYPE_MISMATCH':
			raise CollectionType()

		if line == b'OVERFLOWED':
			raise CollectionOverflow() 

		if line == b'OUT_OF_RANGE':
			raise CollectionIndex()

		return False

	def _recv_lop_get(self):
		ret, value = self._decode_collection('lop')
		if ret == b'NOT_FOUND':
			return None

		if ret == b'TYPE_MISMATCH':
			raise CollectionType()


		if ret == b'UNREADABLE':
			raise CollectionUnreadable() 

		if ret == b'OUT_OF_RANGE' or ret == b'NOT_FOUND_ELEMENT':
			value = []

		return value

	def _recv_sop_get(self):
		ret, value = self._decode_collection('sop')
		if ret == b'NOT_FOUND':
			return None

		if ret == b'TYPE_MISMATCH':
			raise CollectionType()

		if ret == b'UNREADABLE':
			raise CollectionUnreadable() 

		if ret == b'OUT_OF_RANGE' or ret == b'NOT_FOUND_ELEMENT':
			value = set()

		return value

	def _recv_exist(self):
		line = self.handle.readline()
		return line == b'EXIST'

	def _recv_bop_get(self):
		ret, value = self._decode_collection('bop')
		if ret == b'NOT_FOUND':
			return None

		if ret == b'TYPE_MISMATCH':
			raise CollectionType()

		if ret == b'UNREADABLE':
			raise CollectionUnreadable() 

		if ret == b'OUT_OF_RANGE' or ret == b'NOT_FOUND_ELEMENT':
			value = {}

		return value

	def _recv_mget(self):
		ret, value, miss = self._decode_bop_mget()
		if ret == b'NOT_FOUND':
			return None

		if ret == b'TYPE_MISMATCH':
			raise CollectionType()

		if ret == b'UNREADABLE':
			raise CollectionUnreadable() 

		if ret == b'OUT_OF_RANGE' or ret == b'NOT_FOUND_ELEMENT':
			raise CollectionIndex()

		return (value, miss)

	def _recv_smget(self):
		ret, value, miss = self._decode_bop_smget()
		if ret == b'NOT_FOUND':
			return None

		if ret == b'TYPE_MISMATCH':
			raise CollectionType()

		if ret == b'UNREADABLE':
			raise CollectionUnreadable() 

		if ret == b'OUT_OF_RANGE' or ret == b'NOT_FOUND_ELEMENT':
			raise CollectionIndex()

		return (value, miss)




	##########################################################################################
	### decoders
	##########################################################################################
	def _decode_value(self, flags, rlen):
		rlen += 2 # include \r\n
		buf = self.handle.recv(rlen)
		if len(buf) != rlen:
			raise ArcusNodeSocketException("received %d bytes when expecting %d" % (len(buf), rlen))

		if len(buf) == rlen:
			buf = buf[:-2]  # strip \r\n

		val = self.transcoder.decode(flags, buf)

		line = self.handle.readline()
		if line != b'END':
			raise ArcusProtocolException('invalid response expect END but recv: %s' % line)

		return val

	def _decode_collection(self, type):
		if type == 'bop':
			values = {}
		elif type == 'sop':
			values = set()
		else: # lop
			values = []

		while True:
			line = self.handle.readline()
			if line[:5] != b'VALUE' and line[:5] != b'COUNT':
				return (line, values)

			if line[:5] == b'VALUE':
				resp, flags, count = line.split()
				flags = int(flags)
				count = int(count)
			elif line[:5] == b'COUNT':
				cmd, count = line.split(b'=')
				return (cmd, int(count))
			
			for i in range(0, count):
				line = self.handle.readline()
				if type == 'bop': # bop get
					bkey, eflag, length_buf = line.split(b' ', 2)

					if eflag.isdigit(): # eflag not exist
						length = eflag
						eflag = None
						buf = length_buf
					else:
						eflag = eflag.decode('utf-8')
						length, buf = length_buf.split(b' ', 1)

					if bkey.isdigit():
						bkey = int(bkey)
					else:
						bkey = bkey.decode('utf-8')
		
					val = self.transcoder.decode(flags, buf)
					values[bkey] = (eflag, val)
				elif type == 'lop':
					length, buf = line.split(b' ', 1)
					val = self.transcoder.decode(flags, buf)
					values.append(val)
				else: # sop
					length, buf = line.split(b' ', 1)
					val = self.transcoder.decode(flags, buf)
					values.add(val)

		return None

			
	def _decode_bop_mget(self):
		values = {}
		missed_keys = []

		while True:
			line = self.handle.readline()
			if line[:11] == b'MISSED_KEYS':
				dummy, count = line.split(b' ')
				count = int(count)
				for i in range(0, count): 
					line = self.handle.readline()
					missed_keys.append(line.decode('utf-8'))

				continue

			if line[:5] != b'VALUE' and line[:5] != b'COUNT':
				return (line, values, missed_keys)

			ret = line.split()
			key = ret[1].decode('utf-8')
			status = ret[2]

			if status == b'NOT_FOUND':
				missed_keys.append(key)
				continue

			count = 0
			if len(ret) == 5:
				flags = int(ret[3])
				count = int(ret[4])

			val = {}
			for i in range(0, count):
				line = self.handle.readline()
				element, bkey, eflag, length_buf = line.split(b' ', 3)

				if eflag.isdigit(): # eflag not exist
					length = eflag
					eflag = None
					buf = length_buf
				else:
					eflag = eflag.decode('utf-8')
					length, buf = length_buf.split(b' ', 1)

				if bkey.isdigit():
					bkey = int(bkey)
				else:
					bkey = bkey.decode('utf-8')
	
				ret = self.transcoder.decode(flags, buf)
				val[bkey] = (eflag, ret)

			values[key] = val

		return None



	def _decode_bop_smget(self):
		values = []
		missed_keys = []

		while True:
			line = self.handle.readline()
			if line[:11] == b'MISSED_KEYS':
				dummy, count = line.split(b' ')
				count = int(count)
				for i in range(0, count): 
					line = self.handle.readline()
					missed_keys.append(line.decode('utf-8'))

				continue

			if line[:5] != b'VALUE' and line[:5] != b'COUNT':
				return (line, values, missed_keys)

			ret = line.split()
			count = int(ret[1])
			
			for i in range(0, count):
				line = self.handle.readline()
				key, flags, bkey, eflag, length_buf = line.split(b' ', 4)

				if eflag.isdigit(): # eflag not exist
					length = eflag
					eflag = None
					buf = length_buf
				else:
					eflag = eflag.decode('utf-8')
					length, buf = length_buf.split(b' ', 1)

				key = key.decode('utf-8')

				if bkey.isdigit():
					bkey = int(bkey)
				else:
					bkey = bkey.decode('utf-8')
	
				val = self.transcoder.decode(int(flags), buf)
				values.append((bkey, key, eflag, val))

		return None





class EflagFilter:
	def __init__(self, expr = None):
		self.lhs_offset = 0
		self.bit_op = None
		self.bit_rhs = None
		self.comp_op = None
		self.comp_rhs = None

		if expr != None:
			self._parse(expr)

	def get_expr(self):
		expr = ''
		if self.lhs_offset != None:
			expr += '%d' % self.lhs_offset

			if self.bit_op and self.bit_rhs:
				expr += ' %s %s' % (self.bit_op, self.bit_rhs)

			if self.comp_op and self.comp_rhs:
				expr += ' %s %s' % (self.comp_op, self.comp_rhs)

		return expr			

	def _parse(self, expr):
		re_expr = re.compile("EFLAG[ ]*(\[[ ]*([0-9]*)[ ]*\:[ ]*\])?[ ]*(([\&\|\^])[ ]*(0x[0-9a-fA-F]+))?[ ]*(==|\!=|<|>|<=|>=)[ ]*(0x[0-9a-fA-F]+)")

		match = re_expr.match(expr)
		if match == None:
			raise FilterInvalid()
			
		# ( dummy, lhs_offset, dummy, bit_op, bit_rhs, comp_op, comp_rhs )
		g = match.groups()
		dummy_1, self.lhs_offset, dummy_2, self.bit_op, self.bit_rhs, self.comp_op, self.comp_rhs = g
		
		if self.lhs_offset == None:
			self.lhs_offset = 0
		else:
			self.lhs_offset = int(self.lhs_offset)

		if self.comp_op == '==':
			self.comp_op = 'EQ'
		elif self.comp_op == '!=':
			self.comp_op = 'NE'
		elif self.comp_op == '<':
			self.comp_op = 'LT'
		elif self.comp_op == '<=':
			self.comp_op = 'LE'
		elif self.comp_op == '>':
			self.comp_op = 'GT'
		elif self.comp_op == '>=':
			self.comp_op = 'GE'



class ArcusMCPoll(threading.Thread):
	def __init__(self, node_allocator):
		threading.Thread.__init__(self)
		self.epoll = select.epoll()
		self.sock_node_map = {}
		self.node_allocator = node_allocator

	def run(self):
		arcuslog(self, 'epoll start')

		while True:
			events = self.epoll.poll(2)

			if self.node_allocator.shutdown == True:
				arcuslog(self, 'epoll out')
				return

			for fileno, event in events:
				if event & select.EPOLLIN:
					node = self.sock_node_map[fileno]
					node.do_op()

				if event & select.EPOLLHUP:
					print('EPOLL HUP')
					self.epoll.unregister(fileno)
					node = self.sock_node_map[fileno]
					node.disconnect()
					del self.sock_node_map[fileno]


	def register_node(self, node):
		self.epoll.register(node.get_fileno(), select.EPOLLIN | select.EPOLLHUP)

		arcuslog(self, 'regist node: ', node.get_fileno(), node)
		self.sock_node_map[node.get_fileno()] = node
		
		
		
class ArcusMCWorker(threading.Thread):
	def __init__(self, node_allocator):
		threading.Thread.__init__(self)
		self.q = queue.Queue()
		self.poll = ArcusMCPoll(node_allocator)
		self.poll.start()
		self.node_allocator = node_allocator

	def run(self):
		arcuslog(self, 'worker start')

		while True:
			op = self.q.get()
			if self.node_allocator.shutdown == True:
				arcuslog(self, 'worker done')
				self.poll.join()
				return

			if op == None: # maybe shutdown
				continue
		
			arcuslog(self, 'get operation %s(%s:%s) from %s' % (op.request, op.callback, hex(id(op)), op.node))
			node = op.node

			try:
				node.process_request(op.request)
			except Exception as e:
				arcuslog(self, 'operation failed: %s' % str(e))
				op.set_result(e)


	def register_node(self, node):
		self.poll.register_node(node)
		
		


class ArcusMCNodeAllocator:
	def __init__(self, transcoder):
		self.transcoder = transcoder
		self.worker = ArcusMCWorker(self)
		self.worker.start()
		self.shutdown = False
		

	def alloc(self, addr, name):
		ret = ArcusMCNode(addr, name, self.transcoder, self)
		self.worker.register_node(ret)
		return ret


	def join(self):
		self.worker.join()



