import sqlite3 as sq
import os, copy, pickle, random, json, abc, sys
from collections import MutableSequence

from .rabaSetup import RabaConnection, RabaConfiguration
from . import fields as RabaFields

def _recClassCheck(v, cls) :
	if v is cls : return True

	res = False
	for a in v.__bases__ :
		if a is cls :
			return True
		res = res or _recClassCheck(a, cls)
	return res

def isRabaClass(v) :
	return _recClassCheck(v, Raba)

def isRabaList(v) :
	return _recClassCheck(v.__class__, RabaList) or isRabaListPupa(v)

def isRabaListPupa(v) :
	return _recClassCheck(v.__class__, RabaListPupa)

def isRabaObject(v) :
	return isRabaClass(v.__class__) or isRabaObjectPupa(v)

def isRabaObjectPupa(v) :
	return _recClassCheck(v.__class__, RabaPupa)

def isPythonPrimitive(v) :
	primTypes = [int, int, float, bytes, str, memoryview, type(None)]
	for t in primTypes :
		if isinstance(v, t) :
			return True
	return False

_RabaList_instances = {}
def _registerRabaListInstance(lst, anchorObj, relationName) :
	global _RabaList_instances
	key = (anchorObj._runtimeId, relationName)
	_RabaList_instances[key] = lst

def _unregisterRabaListInstance(lst) :
	global _RabaList_instances
	key = (lst.anchorObj._runtimeId, lst.relationName)
	try :
		del(_RabaList_instances[key])
	except KeyError :
		pass

def _getRabaListInstance(anchorObj, relationName) :
	global _RabaList_instances
	key = (anchorObj._runtimeId, relationName)
	return _RabaList_instances[key]

_RabaObject_instances = {}
def _registerRabaObjectInstance(obj) :
	global _RabaObject_instances
	key = (obj.__class__, obj._raba_namespace, obj.raba_id)
	_RabaObject_instances[key] = obj

def _unregisterRabaObjectInstance(obj) :
	global _RabaObject_instances
	key = (obj.__class__, obj._raba_namespace, obj.raba_id)
	
	if not isRabaObjectPupa(obj) :
		for l in obj.rabaLists :
			_unregisterRabaListInstance(l)
	
	try :
		del(_RabaObject_instances[key])
	except KeyError :
		pass

def _getRabaObjectInstance(cls, namespace, raba_id) :
	global _RabaObject_instances
	key = (cls, namespace, raba_id)
	return _RabaObject_instances[key]

class _RabaListPupaSingleton_Metaclass(abc.ABCMeta):

	def __call__(clsObj, *args, **kwargs):
		anchorObj = kwargs['anchorObj']
		relationName = kwargs['relationName']
		length = kwargs['length']

		try :
			return _getRabaListInstance(anchorObj, relationName)
		except KeyError:
			pass
		
		connection = RabaConnection(anchorObj._raba_namespace)

		obj = super(_RabaListPupaSingleton_Metaclass, clsObj).__call__(*args, anchorObj = anchorObj, relationName = relationName, length = length)

		_registerRabaListInstance(obj, anchorObj, relationName)
	
		return obj

class _RabaListSingleton_Metaclass(abc.ABCMeta):
	def __call__(clsObj, *args, **kwargs):

		if 'anchorObj' in kwargs and 'relationName' in kwargs :
			anchorObj = kwargs['anchorObj']
			relationName = kwargs['relationName']

			try :
				return _getRabaListInstance(anchorObj, relationName)
			except KeyError:
				pass

			obj = super(RabaList_Metaclass, clsObj).__call__(*args, **kwargs)

			_registerRabaListInstance(obj, anchorObj, relationName)
			
		return super(_RabaListSingleton_Metaclass, clsObj).__call__(*args, **kwargs)

class _RabaPupaSingleton_Metaclass(type):
	def __call__(clsObj, *args, **kwargs):

		if 'classObj' in kwargs :
			cls = kwargs['classObj']
		else :
			cls = args[0]

		if 'raba_id' in kwargs :
			raba_id = kwargs['raba_id']
		else :
			raba_id = args[1]

		try :
			return _getRabaObjectInstance(cls, cls._raba_namespace, raba_id)
		except KeyError :
			pass
		
		obj = super(_RabaPupaSingleton_Metaclass, clsObj).__call__(*args, **kwargs) 
		_registerRabaObjectInstance(obj)
		 
		return obj
	
class _RabaSingleton_MetaClass(type) :

	_instances = {}

	def __new__(cls, name, bases, dct) :
		if '_raba_abstract' not in dct or not dct['_raba_abstract'] :
			def getFields_rec(name, sqlFields, columns, columnsToLowerCase, dct, bases) :
				i = 0
				if name != "Raba" :
					for base in bases :
						i += getFields_rec(base.__name__, sqlFields, columns, columnsToLowerCase, base.__dict__, base.__bases__)

				for k, v in dct.items() :
					if RabaFields.isField(v) :
						sk = str(k)
						if k.lower() != 'raba_id' and k.lower() != 'json' :
							sqlFields.append(sk)

						columns[sk] = i
						columnsToLowerCase[sk.lower()] = sk
						i += 1
				return i

			sqlFields = []
			columns = {}
			columnsToLowerCase = {}
			getFields_rec(name, sqlFields, columns, columnsToLowerCase, dct, bases)
			columns["raba_id"] = 0
			columns['json'] = 1
			
			try :
				con = RabaConnection(dct['_raba_namespace'])
			except KeyError :
				raise ValueError("The class %s has no defined namespace, please add a valid '_raba_namespace' to class attributes" % name)

			uniqueStr = ''
			if '_raba_uniques' in dct :
				for c in dct['_raba_uniques'] :
					if type(c) is str :
						uniqueStr += 'UNIQUE (%s) ON CONFLICT REPLACE, ' % c
					elif len(c) == 1 :
						uniqueStr += 'UNIQUE (%s) ON CONFLICT REPLACE, ' % c[0]
					else :
						uniqueStr += 'UNIQUE %s ON CONFLICT REPLACE, ' % str(c)
			uniqueStr = uniqueStr[:-2]

			if not con.tableExits(name) :
				idJsonStr = 'raba_id INTEGER PRIMARY KEY, json '
				if len(sqlFields) > 0 :
					if len(uniqueStr) > 0 :
						con.createTable(name, '%s, %s, %s' % (idJsonStr, ', '.join(list(sqlFields)), uniqueStr))
					else :
						con.createTable(name, '%s, %s' % (idJsonStr, ', '.join(list(sqlFields))))
				else :
					con.createTable(name, '%s' % idJsonStr)

				sqlCons = 'INSERT INTO raba_tables_constraints (table_name, constraints) VALUES (?, ?)'
				con.execute(sqlCons, (name, uniqueStr))
			else :
				sql = 'SELECT constraints FROM raba_tables_constraints WHERE table_name = ?'

				cur = con.execute(sql, (name,))
				res = cur.fetchone()

				if res != None and res[0]!= '' and res[0] != uniqueStr :
					sys.stderr.write('Warning: The unique contraints have changed from:\n\t%s\n\nto:\n\t%s.\n-Unique constraints modification is not supported yet-\n' %(res[0], uniqueStr))
					#raise FutureWarning('Warning: The unique contraints have changed from:\n\t%s\n\nto:\n\t%s.\n-Unique constraints modification is not supported yet-\n' %(res[0], uniqueStr))

				cur = con.execute('PRAGMA table_info("%s")' % name)
				tableColumns = set()
				tableColumnsToKeep = []
				tableColumnsToDrop = []

				mustClean = False
				columns = {}
				columns['raba_id'] = 0
				columns['json'] = 1
				i = len(columns)
				for c in cur :
					if c[1] != 'raba_id' and c[1] != 'json' :
						if c[1].lower() not in columnsToLowerCase :
							mustClean = True
							con.dropRabalist(name, c[1])
							tableColumnsToDrop.append(c[1])
						else :
							tableColumnsToKeep.append(c[1])
							columns[columnsToLowerCase[c[1].lower()]] = i
							i += 1
					tableColumns.add(c[1].lower())

				if mustClean :
					con.dropColumnsFromRabaObjTable(name, tableColumnsToKeep)

				mustAlter = False
				for k in columnsToLowerCase :
					if k not in tableColumns :
						con.execute('ALTER TABLE %s ADD COLUMN %s' % (name, columnsToLowerCase[k]))
						mustAlter = True

				if mustClean or mustAlter :
					con.forceCommit()

			dct['columns'] = columns
			dct['columnsToLowerCase'] = columnsToLowerCase

			clsObj = type.__new__(cls, name, bases, dct)
			con.registerRabaClass(clsObj)
			return clsObj

		return type.__new__(cls, name, bases, dct)

	def __call__(cls, *args, **fieldsDct) :
		if cls == Raba :
			return super(_RabaSingleton_MetaClass, cls).__call__(**fieldsDct)

		if 'raba_id' in fieldsDct :
			try :
				return _getRabaObjectInstance(cls, cls._raba_namespace, raba_id)
			except KeyError :
				pass
		else :
			key = None

		params = copy.copy(fieldsDct)
		nonRabaParams = {}
		for p, v in params.items() :
			if p in cls.columns :
				if isRabaObject(v) :
					params[p] = v.getJsonEncoding()
			else :
				nonRabaParams[p] = v
				del(params[p])

		connection = RabaConnection(cls._raba_namespace)
		ret = connection.getRabaObjectInfos(cls.__name__, params)
		
		if ret != None :
			dbLine = ret.fetchone()
		else :
			dbLine = None

		if dbLine != None :
			if ret.fetchone() != None :
				raise ValueError("More than one object fit the arguments you've provided to the constructor")

			raba_id = dbLine[0]
			try :
				return _getRabaObjectInstance(cls, cls._raba_namespace, raba_id)
			except KeyError :
				pass

			obj = Raba.__new__(cls, *args, **nonRabaParams)
			obj._raba__init__(initDbLine = dbLine)
			obj.__init__(*args, **nonRabaParams)

			if not hasattr(cls, '_raba_not_a_singleton') or not getattr(cls, '_raba_not_a_singleton') :
				_registerRabaObjectInstance(obj)
		
		elif len(params) > 0 : # params provided but no result
			raise KeyError("Couldn't find any object that fit the arguments you've provided to the constructor")
		else :
			obj = type.__call__(cls, *args, **fieldsDct)
			obj._raba__init__(**fieldsDct)

		return obj

def freeRegistery() :
	"""Empties all registeries. This is useful if you want to allow the garbage collector to free the memory
	taken by the objects you've already loaded. Be careful might cause some discrepenties in your scripts"""
	freeListRegistery()
	freeObjectRegistery()
	
def freeListRegistery() :
	"""same as freeRegistery() bu only for lists"""
	global _RabaList_instances
	_RabaList_instances = {}
	
def freeObjectRegistery() :
	"""same as freeRegistery() bu only for objects"""
	global _RabaObject_instances
	_RabaObject_instances = {}

def removeFromRegistery(obj) :
	"""Removes an object/rabalist from registery. This is useful if you want to allow the garbage collector to free the memory
	taken by the objects you've already loaded. Be careful might cause some discrepenties in your scripts. For objects,
	cascades to free the registeries of related rabalists also"""
	
	if isRabaObject(obj) :
		_unregisterRabaObjectInstance(obj)
	elif isRabaList(obj) :
		_unregisterRabaListInstance(obj)	

class RabaPupa(object, metaclass=_RabaPupaSingleton_Metaclass) :
	"""One of the founding principles of RabaDB is to separate the storage from the code. Fields are stored in the DB while the processing only depends
	on your python code. This approach ensures a higher degree of stability by preventing old objects from lurking inside the DB before popping out of nowhere several decades afterwards.
	According to this apparoach, raba objects are not serialised but transformed into pupas before being stored. A pupa is a very light object that contains only a reference
	to the raba object class, and it's unique raba_id. Upon asking for one of the attributes of a pupa, it magically transforms into a full fledged raba object. This process is completly transparent to the user. Pupas also have the advantage of being light weight and also ensure that the only raba objects loaded are those explicitely accessed, thus potentialy saving a lot of memory.
	For a pupa self._rabaClass refers to the class of the object "inside" the pupa.
	"""

	def __init__(self, classObj, raba_id) :
		self._rabaClass = classObj
		self.raba_id = raba_id
		self._raba_namespace = classObj._raba_namespace
		self.__doc__ = classObj.__doc__

	def develop(self) :
		def getAttr(name) :
			return object.__getattribute__(self, name)

		def setAttr(name, value) :
			object.__setattr__(self, name, value)

		rabaClass = getAttr('_rabaClass')
		uniqueId = getAttr('raba_id')
		connection = RabaConnection(getAttr('_raba_namespace'))
		dbLine = connection.getRabaObjectInfos(getAttr('_rabaClass').__name__, {'raba_id' : uniqueId}).fetchone()
		setAttr('__class__', rabaClass)
		purge = getAttr('__dict__').keys()
		for k in list(purge) :
			delattr(self, k)