import numpy as np
from collections import deque
import re
import sys
from typing import List, Tuple, Optional, Union, Dict, Type


class ListStrCastNode(object): 

	def __init__(
			self,
			start,
			end,
			parent = None,
			path = [],
			):
		self.start = start
		self.end = end
		self.parent = parent
		self.utils = StrCastUtils()
		self.path = path 

	def top_child(
			self,
			children,
			):
		return list(children)[-1] 

	def pop_child(
			self,
			children,
			pos,
			):
		return children.pop(pos) 

	def is_child(
			self,
			start1,
			end1,
			start2,
			end2,
			):
		if start1 > start2 and end1 < end2:
			return True
		return

	def is_leaf(
			self,
			children,
			pos,
			):
		if self.top_child(children) == pos:
			return True
		return

	def is_chars_leaf(
			self,
			chars_dict,
			pos,
			):
		if self.top_child(chars_dict) == pos:
			return True
		return

	def get_children(
			self,
			internal_nodes = True,
			leaves = True,
			):
		if internal_nodes and leaves:
			return [
				child for name, child in self.__dict__.items() 
					if 'child' in name or 'leaf' in name
			]
		elif internal_nodes and not leaves:
			return [
				child for name, child in self.__dict__.items() 
					if 'child' in name
			]
		elif leaves and not internal_nodes:
			return [
				child for name, child in self.__dict__.items() 
					if 'leaf' in name
			]
		return

	def child_names(
			self, 
			node,
			):
		return [
			name for name in self.__dict__ 
				if 'child' in name
		]

	def leaf_names(
			self,
			node,
			):
		return [
			name for name in self.__dict__
				if 'leaf' in name
		]

	def child_path(self):
		path = [] 
		if self.path:
			path = [pos for pos in self.path] 
		path.append(len(self.get_children(self)))
		return path

	def add_child(
			self, 
			start,
			end,
			):
		if self.child_names(self):
			name = (
				'child'
				+ str(int(re.findall(
					r'\d+', 
					self.child_names(self)[-1])[0]) 
				+ 1)
			)
		else:
			name = 'child0'
		setattr(
			self, name, ListStrCastNode(
				start, end, self,
				self.child_path()
			)
		) 
		return getattr(self, name) 

	def add_leaf(
			self,
			start,
			end, 
			s,
			chars = False
			):
		name = None
		if self.leaf_names(self):
			name = (
				'leaf'
				+ str(int(re.findall(
					r'\d+', 
					self.leaf_names(self)[-1])[0])
				+ 1)
			)
		else:
			name = 'leaf0'
		leaf = self.utils.cleanup_str(
			s, start + 1,
			end, 'list'
		)
		if chars:
			setattr(self, name, tuple(leaf))
		else:
			setattr(self, name, leaf)
		return getattr(self, name) 

	def is_numpy_dtype(
			self,
			val: str,
			) -> bool:
		numpy_dtype_suffixes = (
			'_INT32',
			'_INT64',
			'_FLOAT32',
			'_FLOAT64',
			'_COMPLEX64',
			'_COMPLEX128',
		)
		for numpy_dtype_suffix in numpy_dtype_suffixes:
			if numpy_dtype_suffix in val:
				return True
		return False

class StrCastUtils(object):

	def is_numpy_dtype(
			self,
			val: str,
			) -> bool:
		numpy_dtype_suffixes = (
			'_INT32',
			'_INT64',
			'_FLOAT32',
			'_FLOAT64',
			'_FLOAT128',
			'_COMPLEX64',
			'_COMPLEX128',
		)
		for numpy_dtype_suffix in numpy_dtype_suffixes:
			if numpy_dtype_suffix in val:
				return True
		return False

	def parse_numpy_dtype(
			self,
			val: str,
			) -> Tuple[str]:
		dtype_name = \
			val.partition('_')[-1].replace('_', '').lower() 
		parsed_val = val.partition('_')[0]
		return parsed_val, dtype_name

	def xml_str_cast(
			self,
			val,
			):
		if type(val.text) == type(None):
			return	 
		else:
			if val.text.lower() == 'true':
				return True 
			elif val.text.lower() == 'false':
				return False 
			elif '[' in val.text:
				cast = StrCast()
				return cast.str_to_list(val)
			elif self.is_numpy_dtype(val.text):
				parsed_str_val, dtype_name = \
					self.parse_numpy_dtype(val.text)
				return self.__str_cast(val.text, dtype_name)			 
			elif val.text == '.' or re.search('[a-zA-Z]', val.text):
				return self.__str_cast(val.text, 'str')
			elif re.search('[\.]', val.text):
				return self.__str_cast(val.text, 'float')
			else:
				return self.__str_cast(val.text, 'int')

	def std_str_cast(
			self,
			val,
			):
		if type(val) == type(None):
			return	 
		else:
			if val.lower() == 'true':
				return True 
			elif val.lower() == 'false':
				return False 
			elif '[' in val:
				cast = StrCast()
				return cast.str_to_list(val)
			elif self.is_numpy_dtype(val):
				parsed_val, dtype_name = \
					self.parse_numpy_dtype(val)
				return self.__str_cast(parsed_val, dtype_name)			   
			elif val == '.' or re.search('[a-zA-Z]', val):
				return self.__str_cast(val, 'str')
			elif re.search('[\.]', val):
				return self.__str_cast(val, 'float')
			else:
				return self.__str_cast(val, 'int')

	def str_cast(
			self, 
			val,
			):
		if hasattr(val, 'text'):
			return self.xml_str_cast(val)
		else:
			return self.std_str_cast(val)

	def __str_cast(
			self, 
			str_val,
			type_str,
			):
		type_lookup = {
			'int' : int,
			'float' : float,
			'str' : str,
			'int32' : np.int32,
			'int64' : np.int64,
			'float32' : np.float32,
			'float64' : np.float64,
			'float128' : np.float128,
			'complex64' : np.complex64,
			'complex128' : np.complex128
		} 
		return type_lookup[type_str](str_val)

	def except_the_kitchen_sink_regex(
			self, 
			str_struct
			):
		struct = re.findall(
			r'\"[^\"]*\"|\S*[A-Za-z]\S*[0-9]\S*|'
			'[0-9]?\S*[A-Za-z]\S*|'
			'-?\.?\d+\.?\d*|\d+',
			str_struct
		)
		return struct

	def str_cast_list_elements(
			self,
			str_list,
			):
		parsed_str_list = self.except_the_kitchen_sink_regex(
			str_list
		)
		return self.cleanup_str_list(
			parsed_str_list 
		)

	def cleanup_str_list(
			self,
			parsed_str_list,
			) -> List[str]:
		parsed_str_list = [
			list_element if '"' in list_element 
			else list_element.replace(',', '') 
				for list_element in parsed_str_list
		]
		parsed_str_list = [
			list_element.replace('"', '') for list_element
			in parsed_str_list
		]
		parsed_str_list = [
			self.str_cast(list_element) for list_element
			in parsed_str_list
		]
		return parsed_str_list
		

	def str_cast_dict_elements(
			self,
			str_dict,
			): #Come back to this later
		return	

#get rid of *args
	def str_cast_struct_elements(
			self,
			str_struct,
			*args,
			):
		str_cast_struct_dict = {
			'list' : self.str_cast_list_elements(str_struct), 
			'dict' : self.str_cast_dict_elements(str_struct),
		}
		struct_type = args[0]
		return str_cast_struct_dict[struct_type]

#get rid of *args
	"""
	def find_matching(
			self,
			s,
			*args,
			):
		matching_dict = {}
		stack = []
		start, end = args
		for pos, i in enumerate(s):
			if i == start:
				stack.append(pos)
			elif i == end:
				matching_dict[stack.pop()] = pos
		return matching_dict
	"""

	def is_start_end(self, char, start, end, matching_dict):
		if start == end:
			if char == start:
				if None in matching_dict.values():
					return 'end'
				return 'start'
			return ''
		if char == start:
			return 'start'
		elif char == end:
			return 'end'
		return ''

	def find_matching(
			self, 
			s,
			start,
			end,
			):
		matching_dict = {}
		stack = []
		for pos, i in enumerate(s):
			if self.is_start_end(i, start, end, matching_dict) == "start":
				matching_dict[pos] = None
				stack.append(pos)
			elif self.is_start_end(i, start, end, matching_dict) == "end":
				matching_dict[stack.pop()] = pos
		return matching_dict

	def num_lists(
			self,
			s,
			):
		return self.num_structs(s, 'list')

	def num_dicts(
			self,
			s,
			):
		return self.num_structs(s, 'dict')

	def num_structs(
			self,
			s,
			struct_type,
			):
		num_structs_dict = { 
			'list' : self.find_matching(s, '[', ']'), 
			'dict' : self.find_matching(s, '{', '}')
		}
		return len(num_structs_dict[struct_type])

	def nested_list_insert(
			self,
			lst,
			val,
			path,
			):
		if isinstance(path, list):
			path = deque(path)
		self.__nested_list_insert(lst, val, path)
		return lst

	def __nested_list_insert(
			self,
			lst,
			val,
			path,
			):
		if path:
			pos = path.popleft()
			return self.__nested_list_insert(lst[pos], val, path)
		lst.append(val)

	def sort_dict(
			self,
			d,
			*args,
			**kwargs,
			):
		args = list(args)
		for index, arg in enumerate(args):
			if arg == 'key':
				args[index] = 0
			if arg == 'value':
				args[index] = 1
		kwargs_keys = [key for key, value in kwargs.items()]
		return {
			key : value for key, value in sorted(
				d.items(), key = lambda item : item[args[0]], 
				reverse = kwargs[kwargs_keys[0]]
			)
		}

	def cleanup_str(
			self, 
			s, 
			start, 
			end, 
			*args,
			):
		return self.str_cast_struct_elements(s[start : end], *args)

	def chars(
			self, 
			s, 
			*args,
			):
		chars_dict = {}
		prev_bracket = {}
		for index, char in enumerate(s):
			if char == args[0]:
				if prev_bracket:
					start, end = list(prev_bracket)[-1], index
					if prev_bracket[start] == args[0]:
						if (end - start) != 1:
							chars_dict[start] = self.cleanup_str(
								s, start, end,
								args[2]
							)
					elif prev_bracket[start] == args[1]:
						if (end - start) != 3:
							chars_dict[start] = self.cleanup_str(
									s, start, end,
									args[2]
								)
				prev_bracket = {index : args[0]} 
			elif char == args[1]:
				start, end = list(prev_bracket)[-1], index
				if prev_bracket[start] == args[1]:
					if (end - start) != 1:
						chars_dict[start] = self.cleanup_str(
							s, start, end,
							args[2]
						)
				prev_bracket = {index : args[1]} 
		return chars_dict

	
class StrCast(object):

	def __init__(self):
		self.utils = StrCastUtils()

	def adjust_recursion_limit(
			self,
			s,
			):
		if self.utils.num_lists(s) > 100:
			sys.setrecursionlimit(int(10e8))

	def start_end(
			self, 
			q_s, 
			pos = None,
			):
		if not pos:
			start = list(q_s[-1])[0]
			end = q_s[-1][start]
			return start, end 
		start = list(q_s[pos])[0]
		end = q_s[pos][start]
		return start, end 
		
	def list_init_queue(
			self, 
			s,
			):
		return deque([
			{start : end} for start, end in self.utils.sort_dict(
				self.utils.find_matching(s, '[', ']'),
				'key', reverse = True).items()
		])

	def list_init_children(
			self, 
			s,
			):
		return self.utils.sort_dict(
			self.utils.find_matching(s, '[', ']'),
			'value', reverse = True
		)

	def list_init_stack(
			self,
			s,
			init_queue = None,
			):
		if init_queue:
			return deque([init_queue.pop()])
		else:
			return deque()

	def list_str_tree(
			self,
			s,
			**qscpn,
			):
		if not qscpn:
			self.adjust_recursion_limit(s)
			qscpn['queue'] = self.list_init_queue(s)
			if len(qscpn['queue']) == 1:
				qs, qe = self.start_end(qscpn['queue']) 
				node = ListStrCastNode(qs, qe)
				node.add_leaf(qs, qe, s)
				return node
			qscpn['stack'] = self.list_init_stack(s, qscpn['queue'])
			qscpn['children'] = self.list_init_children(s) 
			qscpn['pos'] =	self.start_end(qscpn['queue'])[0]
			qscpn['node'] = ListStrCastNode(*self.start_end(qscpn['stack']))
			qscpn['node'].pop_child(qscpn['children'], 0)
		queue, stack, children, pos, node = qscpn.values()
		qs, qe = self.start_end(queue) 
		ss, se = self.start_end(stack) 
		if len(queue) > 1:
			if se > qe:
				if node.is_leaf(children, pos):
					node.pop_child(children, qs)
					node.add_leaf(qs, qe, s)
					queue.pop()
					pos = self.start_end(queue)[0]
					qscpn = dict(zip(
						qscpn.keys(), (
							queue,
							stack, 
							children, 
							pos, 
							node,
						)
					))
					self.list_str_tree(s, **qscpn)
				else:
					node.pop_child(children, qs)
					child = node.add_child(qs, qe)
					stack.append(queue.pop())
					pos = self.start_end(queue)[0]
					qscpn = dict(zip(
						qscpn.keys(), (
							queue, 
							stack, 
							children,
							pos,
							child,
						)
					))
					self.list_str_tree(s, **qscpn)
			elif qe > se:
				stack.pop()
				qscpn = dict(zip(
					qscpn.keys(), (
						queue, 
						stack, 
						children, 
						pos, 
						node.parent,
					)
				))
				self.list_str_tree(s, **qscpn)
		if children:
			if node.is_child(
					qs, qe, ss, 
					se
				):
				if node.is_leaf(children, pos):
					node.pop_child(children, qs)
					node.add_leaf(qs, qe, s)
					queue.pop()
			else:
				stack.pop()
				qscpn = dict(zip(
					qscpn.keys(), ( 
						queue, 
						stack,
						children, 
						pos, 
						node.parent
					)
				))
				self.list_str_tree(s, **qscpn)
		return node

	def list_str_tree_to_list(
			self, 
			tree, 
			lst,
			):
		self.__list_str_tree_to_list(tree, lst)
		return lst

	
	def __list_str_tree_to_list(
			self, 
			node, 
			lst,
			):
		if node.get_children():
			for child in node.get_children():
				if isinstance(child, type(node)):
					self.utils.nested_list_insert(lst, [], node.path)
					self.__list_str_tree_to_list(child, lst)
				if isinstance(child, list):
					self.utils.nested_list_insert(lst, child, node.path)
		
	def str_to_list(
			self, 
			s,
			):
		tree = self.list_str_tree(s)
		lst = []
		if self.utils.num_lists(s) == 1:
			return self.list_str_tree_to_list(tree, lst)[0]
		return self.list_str_tree_to_list(tree, lst)
	
def strcast(
		s,
		):
	str_cast = StrCast()
	return str_cast.utils.str_cast(s)

