from OpenGL import GL
import numpy as np

from .GLInfo import GLInfo
from .GLObject import GLObject
from .GlassConfig import GlassConfig

class BO(GLObject):

	def __init__(self, context_shared=True)->None:
		GLObject.__init__(self, context_shared=context_shared)
		self._nbytes:int = 0
		self._draw_type:GLInfo.draw_types = GL.GL_STATIC_DRAW

	def delete(self)->None:
		GLObject.delete(self)
		self._nbytes = 0

	def bufferData(self, value_array, draw_type:GLInfo.draw_types=GL.GL_STATIC_DRAW)->None:
		if GlassConfig.debug and self.__class__.__name__ in ["FBO", "RBO"]:
			raise AttributeError("'" + self.__class__.__name__ + "' object has no attribute 'bufferData'")

		array_bytes = 0
		if isinstance(value_array, np.ndarray):
			array_bytes = value_array.nbytes
		elif isinstance(value_array, bytes):
			array_bytes = len(value_array)
		elif isinstance(value_array, bytearray):
			array_bytes = len(value_array)
			value_array = bytes(value_array)
		else:
			value_array = np.array(value_array)
			array_bytes = value_array.nbytes

		self.bind()
		GL.glBufferData(self.__class__._basic_info["target_type"], array_bytes, value_array, draw_type)
		self._nbytes = array_bytes
		self._draw_type = draw_type

	def malloc(self, nbytes:int, draw_type:GLInfo.draw_types=GL.GL_STATIC_DRAW)->None:
		if GlassConfig.debug:
			if self.__class__.__name__ in ["FBO", "RBO"]:
				raise AttributeError("'" + self.__class__.__name__ + "' object has no attribute 'malloc'")

		self.bind()
		GL.glBufferData(self.__class__._basic_info["target_type"], nbytes, None, draw_type)
		self._nbytes = nbytes
		self._draw_type = draw_type

	def memmove(self, old_start:int, nbytes:int, new_start:int)->None:
		if GlassConfig.debug:
			if old_start < 0:
				raise ValueError("source start position should be positive, " + str(old_start) + " is passed")
			elif old_start >= self.nbytes:
				raise ValueError("source start position is out of range, max position is " + str(self._nbytes-1), ", " + str(old_start) + " is passed")

			if new_start < 0:
				raise ValueError("dest start position should be positive, " + str(new_start) + " is passed")
			elif new_start >= self.nbytes:
				raise ValueError("dest start position is out of range, max position is " + str(self._nbytes-1), ", " + str(new_start) + " is passed")

			if nbytes < 0:
				raise ValueError("'nbytes' should be positive, " + str(nbytes) + " is passed")

			if old_start + nbytes > self._nbytes:
				raise ValueError("source buffer end position is out of range, max position is " + str(self._nbytes) +  ", " + str(old_start + nbytes) + " is applied.")

		if self._nbytes == 0 or nbytes == 0 or old_start == new_start:
			return

		temp_bo_id = GL.glGenBuffers(1)
		GL.glBindBuffer(GL.GL_COPY_WRITE_BUFFER, temp_bo_id)
		GL.glBufferData(GL.GL_COPY_WRITE_BUFFER, nbytes, None, self.draw_type)

		GL.glBindBuffer(GL.GL_COPY_READ_BUFFER, self._id)
		GL.glCopyBufferSubData(GL.GL_COPY_READ_BUFFER, GL.GL_COPY_WRITE_BUFFER, old_start, 0, nbytes)
		
		GL.glBindBuffer(GL.GL_COPY_WRITE_BUFFER, 0)
		GL.glBindBuffer(GL.GL_COPY_READ_BUFFER, 0)
		
		self.bufferSubData(old_start, nbytes, bytes(nbytes))
		real_delta = 0
		if old_start-new_start > 0:
			real_delta = old_start-new_start
			self.copy_to(new_start, real_delta, self, new_start+nbytes)
		else:
			real_delta = min(new_start+nbytes, self._nbytes)-(old_start+nbytes)
			self.copy_to(old_start+nbytes, real_delta, self, old_start)

		GL.glBindBuffer(GL.GL_COPY_WRITE_BUFFER, self._id)
		GL.glBindBuffer(GL.GL_COPY_READ_BUFFER, temp_bo_id)
		GL.glCopyBufferSubData(GL.GL_COPY_READ_BUFFER, GL.GL_COPY_WRITE_BUFFER, 0, new_start, nbytes)
		GL.glBindBuffer(GL.GL_COPY_WRITE_BUFFER, 0)
		GL.glBindBuffer(GL.GL_COPY_READ_BUFFER, 0)
		GL.glDeleteBuffers(1, np.array([temp_bo_id]))
		temp_bo_id = 0

	def bufferSubData(self, start:int, nbytes:int, value_array)->None:
		if GlassConfig.debug:
			if self.__class__.__name__ in ["FBO", "RBO"]:
				raise AttributeError("'" + self.__class__.__name__ + "' object has no attribute 'bufferSubData'")

		array_nbytes = 0
		if isinstance(value_array, np.ndarray):
			array_nbytes = value_array.nbytes
			value_array = value_array.tobytes()
		elif isinstance(value_array, bytes):
			array_nbytes = len(value_array)
		elif isinstance(value_array, bytearray):
			array_nbytes = len(value_array)
			value_array = bytes(value_array)
		else:
			value_array = np.array(value_array)
			array_nbytes = value_array.nbytes
			value_array = value_array.tobytes()

		if GlassConfig.debug:
			if start < 0:
				raise ValueError("Memory start position should be positive, " + str(start) + " is passed")
			
			if nbytes < 0:
				raise ValueError("'nbytes' should be positive, " + str(nbytes) + " is passed")

			if start >= self._nbytes:
				raise ValueError("Memory start position is out of range, max position is " + str(self._nbytes-1) + ", " + str(start) + " is passed.")

			if start + nbytes > self._nbytes:
				raise ValueError("Memory end position is out of range, max position is " + str(self._nbytes) +  ", " + str(start + nbytes) + " is applied.")

			if nbytes > array_nbytes:
				raise ValueError("Need copy data is over value_array's size. Max data size is " + str(value_array.nbytes) + " bytes, " + str(nbytes) + " bytes is needed.")

		if nbytes == 0:
			return

		self.bind()
		GL.glBufferSubData(self.__class__._basic_info["target_type"], start, nbytes, value_array)

	def copy_to(self, src_start:int, nbytes:int, dest_bo, dest_start:int)->None:
		if GlassConfig.debug:
			if self.__class__.__name__ in ["FBO", "RBO"]:
				raise AttributeError("'" + self.__class__.__name__ + "' object has no attribute 'copy_to'")

			if src_start < 0:
				raise ValueError("source start position should be positive, " + str(src_start) + " is passed")
			elif src_start >= self.nbytes:
				raise ValueError("source start position is out of range, max position is " + str(self._nbytes-1) + ", " + str(src_start) + " is passed")

			if dest_start < 0:
				raise ValueError("dest start position should be positive, " + str(dest_start) + " is passed")
			elif dest_start >= dest_bo.nbytes:
				raise ValueError("dest start position is out of range, max position is " + str(dest_bo._nbytes-1) + ", " + str(dest_start) + " is passed")
			
			if nbytes < 0:
				raise ValueError("'nbytes' should be positive, " + str(nbytes) + " is passed")

			if src_start + nbytes > self._nbytes:
				raise ValueError("source buffer end position is out of range, max position is " + str(self._nbytes) +  ", " + str(src_start + nbytes) + " is applied.")

		if self._nbytes == 0 or nbytes == 0:
			return

		real_nbytes = min(nbytes, dest_bo._nbytes-dest_start)
		if self._id != dest_bo._id:
			GL.glBindBuffer(GL.GL_COPY_READ_BUFFER, self._id)
			GL.glBindBuffer(GL.GL_COPY_WRITE_BUFFER, dest_bo._id)
			GL.glCopyBufferSubData(GL.GL_COPY_READ_BUFFER, GL.GL_COPY_WRITE_BUFFER, src_start, dest_start, real_nbytes)
			GL.glBindBuffer(GL.GL_COPY_READ_BUFFER, 0)
			GL.glBindBuffer(GL.GL_COPY_WRITE_BUFFER, 0)
		else:
			temp_bo_id = GL.glGenBuffers(1)
			GL.glBindBuffer(GL.GL_COPY_WRITE_BUFFER, temp_bo_id)
			GL.glBufferData(GL.GL_COPY_WRITE_BUFFER, real_nbytes, None, self.draw_type)

			GL.glBindBuffer(GL.GL_COPY_READ_BUFFER, self._id)
			GL.glCopyBufferSubData(GL.GL_COPY_READ_BUFFER, GL.GL_COPY_WRITE_BUFFER, src_start, 0, real_nbytes)

			GL.glBindBuffer(GL.GL_COPY_READ_BUFFER, temp_bo_id)
			GL.glBindBuffer(GL.GL_COPY_WRITE_BUFFER, self._id)
			GL.glCopyBufferSubData(GL.GL_COPY_READ_BUFFER, GL.GL_COPY_WRITE_BUFFER, 0, dest_start, real_nbytes)

			GL.glBindBuffer(GL.GL_COPY_READ_BUFFER, 0)
			GL.glBindBuffer(GL.GL_COPY_WRITE_BUFFER, 0)

			GL.glDeleteBuffers(1, np.array([temp_bo_id]))
			temp_bo_id = 0

	def shallow_copy_to(self, dest_bo)->None:
		self_members = dir(self)
		dest_members = dir(dest_bo)
		for key in self_members:
			if key in dest_members and not (key.startswith("__") and key.endswith("__")) and not callable(getattr(self, key)):
				setattr(dest_bo, key, getattr(self, key))

	@property
	def nbytes(self)->int:
		if GlassConfig.debug:
			if self.__class__.__name__ in ["FBO", "RBO"]:
				raise AttributeError("'" + self.__class__.__name__ + "' object has no attribute 'nbytes'")

		return self._nbytes

	@property
	def empty(self)->bool:
		return (self._nbytes == 0)

	@property
	def draw_type(self)->GLInfo.draw_types:
		if GlassConfig.debug:
			if self.__class__.__name__ in ["FBO", "RBO"]:
				raise AttributeError("'" + self.__class__.__name__ + "' object has no attribute 'draw_type'")
			
		return self._draw_type