text: # -*- coding: utf-8 -*-
  # -----------------------------------------------------------------------------
  # Copyright (c) Vispy Development Team. All Rights Reserved.
  # Distributed under the (new) BSD License. See LICENSE.txt for more info.
  # -----------------------------------------------------------------------------
  
  import numpy as np
  from os import path as op
  from traceback import extract_stack, format_list
  import weakref
  
  from . globject import GLObject
  from . CPUData import CPUData
  from ..util import logger
  
  
  # ------------------------------------------------------------ Buffer class ---
  class Buffer(GLObject):
      """Generic GPU buffer.
  
      A generic buffer is an interface used to upload data to a GPU array buffer
      (ARRAY_BUFFER or ELEMENT_ARRAY_BUFFER). It keeps track of
      buffer size but does not have any CPU storage. You can consider it as
      write-only.
  
      The `set_data` is a deferred operation: you can call it even if an OpenGL
      context is not available. The `update` function is responsible to upload
      pending data to GPU memory and requires an active GL context.
  
      The Buffer class only deals with data in terms of bytes; it is not
      aware of data type or element size.
  
      Parameters
      ----------
      data : ndarray | None
          Buffer data.
      nbytes : int | None
          Buffer byte size.
      use_cpu : bool | False
          If set to True, a local copy of the data is kept on CPU memory. This
          allows updating non-contiguous chunks of data before uploading to GPU.
      immediate_upload : bool | True
          If set to False (requires use_cpu), changes to the local CPU memory
          are not immediately sent to the GPU; rather, explicit calls to
          upload_from_local method must be made.
      """
  
      def __init__(self, data=None, nbytes=None, use_cpu=False,
                   immediate_upload=True):
          GLObject.__init__(self)
          self._views = weakref.WeakSet()  # Views on this buffer
          self._valid = True  # To invalidate buffer views
          self._nbytes = 0  # Bytesize in bytes, set in resize_bytes()
          if not use_cpu and not immediate_upload:
              raise ValueError('Uploads are necessarily immediate without a '
                               'local CPU buffer.')
          self._use_cpu = use_cpu
          self._immediate_upload = immediate_upload
          self._cpu_data = None  # type: CPUData # Will be set if use_cpu is True
  
          # Set data
          if data is not None:
              if nbytes is not None:
                  raise ValueError("Cannot specify both data and nbytes.")
              self.set_data(data, copy=False)
          elif nbytes is not None:
              self.resize_bytes(nbytes)
  
      @property
      def nbytes(self):
          """Buffer size in bytes"""
          return self._nbytes
  
      @property
      def use_cpu(self):
          """ Whether a local copy of data is kept on CPU memory """
  
          return self._use_cpu
  
      @property
      def immediate_upload(self):
          """ Whether changes to the local CPU memory must be immediately sent to GPU """
  
          return self._immediate_upload
  
      @property
      def cpu_data(self):
          """ Local copy of data kept on CPU memory (CPUData object) """
  
          if not self._use_cpu:
              raise RuntimeError("Buffer has no local CPU buffer.")
  
          return self._cpu_data
  
      @cpu_data.setter
      def cpu_data(self, data):
          """ Local copy of data kept on CPU memory (CPUData object) """
  
          if not self._use_cpu:
              raise RuntimeError("Buffer has no local CPU buffer.")
  
          self.set_data(data)
  
      @property
      def cpu_data_bytes(self):
          """ Local CPU data, viewed as a mere bytes vector """
  
          if not self._use_cpu:
              raise RuntimeError("Buffer has no local CPU buffer.")
  
          return self._cpu_data.view(np.uint8).ravel()
  
      def upload_GPU(self):
          """ Upload changes on the local CPU data to GPU (deferred operation). """
  
          if not self._use_cpu:
              raise RuntimeError("Buffer has no local CPU buffer.")
  
          if self._cpu_data.pending_data is None:
              # no waiting change
              return
  
          start, stop = self._cpu_data.pending_data
          data = self.cpu_data_bytes[start:stop]
          self._glir.command('DATA', self._id, start, data)
          self._cpu_data.reset_pending_data()
  
      def set_subdata(self, data, offset=0, copy=False):
          """Set a sub-region of the buffer (deferred operation).
  
          Parameters
          ----------
          data : ndarray
              Data to be uploaded
          offset: int
              Offset in buffer where to start copying data (in bytes)
          copy: bool | False
              Since the operation is deferred, data may change before
              data is actually uploaded to GPU memory.
              Asking explicitly for a copy will prevent this behavior.
          """
  
          data = np.array(data, copy=copy)
          nbytes = data.nbytes
  
          if offset < 0:
              raise ValueError("Offset must be positive")
          elif (offset + nbytes) > self._nbytes:
              raise ValueError("Data does not fit into buffer")
  
          # If the whole buffer is to be written, we clear any pending data
          # (because they will be overwritten anyway)
          if nbytes == self._nbytes and offset == 0:
              self._glir.command('SIZE', self._id, nbytes)
  
          # Set local data and/or upload
          if self._use_cpu:
              if self._cpu_data is None:
                  raise RuntimeError('Use first set_data to set the local CPU '
                                     'data.')
              # copy bytes, this will also trigger immediate upload if
              # self._immediate_upload is true
              self.cpu_data_bytes[offset:offset+nbytes] = data.view(np.uint8)
          else:
              self._glir.command('DATA', self._id, offset, data)
  
      def set_data(self, data, copy=False):
          """Set data in the buffer (deferred operation).
  
          This completely resets the size and contents of the buffer.
  
          Parameters
          ----------
          data : ndarray
              Data to be uploaded
          copy: bool | False
              Since the operation is deferred, data may change before
              data is actually uploaded to GPU memory.
              Asking explicitly for a copy will prevent this behavior.
              If a local CPU buffer is used, this value will be ignored as
              data will be copied anyway.
          """
          if self._use_cpu:
              # data needs to be copied any way
              copy = True
          data = np.array(data, copy=copy)
          nbytes = data.nbytes
  
          if nbytes != self._nbytes:
              self.resize_bytes(nbytes)
          else:
              # Use SIZE to discard any previous data setting
              self._glir.command('SIZE', self._id, nbytes)
  
          # Set local data and/or upload
          if self._use_cpu:
              self._cpu_data = data.view(CPUData) # type: CPUData
              if self._immediate_upload:
                  self.upload_GPU()
                  self._cpu_data.register_action(self.upload_GPU)
          else:
              if nbytes:
                  # Only set data if there *is* data
                  self._glir.command('DATA', self._id, 0, data)
  
      def resize_bytes(self, size):
          """Resize this buffer (deferred operation).
  
          Parameters
          ----------
          size : int
              New buffer size in bytes.
          """
          self._nbytes = size
  
          self._glir.command('SIZE', self._id, size)
  
          # Invalidate any view on this buffer
          for view in self._views:
              view._valid = False
          self._views = weakref.WeakSet()
  
          # Reset CPU buffer
          if self._use_cpu:
              self._cpu_data = None
  
  
  # -------------------------------------------------------- DataBuffer class ---
  class DataBuffer(Buffer):
      """GPU data buffer that is aware of data type and elements size
  
      Parameters
      ----------
      data : ndarray | None
          Buffer data.
      """
  
      def __init__(self, data=None, use_cpu=False, immediate_upload=True):
          self._size = 0  # number of elements in buffer, set in resize_bytes()
          self._dtype = None
          self._stride = 0
          self._itemsize = 0
          self._last_dim = None
          Buffer.__init__(self, data,
                          use_cpu=use_cpu, immediate_upload=immediate_upload)
  
      def _prepare_data(self, data):
          # Can be overrriden by subclasses
          if not isinstance(data, np.ndarray):
              raise TypeError("DataBuffer data must be numpy array.")
          return data
  
      def set_subdata(self, data, offset=0, copy=False, **kwargs):
          """Set a sub-region of the buffer (deferred operation).
  
          Parameters
          ----------
          data : ndarray
              Data to be uploaded
          offset: int
              Offset in buffer where to start copying data (i.e. index of
              starting element).
          copy: bool
              Since the operation is deferred, data may change before
              data is actually uploaded to GPU memory.
              Asking explicitly for a copy will prevent this behavior.
          **kwargs : dict
              Additional keyword arguments.
  
          """
          data = self._prepare_data(data, **kwargs)
          offset = offset * self.itemsize
          Buffer.set_subdata(self, data=data, offset=offset, copy=copy)
  
      def set_data(self, data, copy=False, **kwargs):
          """Set data (deferred operation)
  
          Parameters
          ----------
          data : ndarray
              Data to be uploaded
          copy: bool
              Since the operation is deferred, data may change before
              data is actually uploaded to GPU memory.
              Asking explicitly for a copy will prevent this behavior.
          **kwargs : dict
              Additional arguments.
          """
          data = self._prepare_data(data, **kwargs)
          self._dtype = data.dtype
          # This works around some strange NumPy bug where a float32 array
          # of shape (155407, 1) was said to have strides
          # (4, 9223372036854775807), which is crazy
          self._stride = data.strides[-1]
          self._itemsize = self._dtype.itemsize
          Buffer.set_data(self, data=data, copy=copy)
  
      @property
      def dtype(self):
          """Buffer dtype"""
          return self._dtype
  
      @property
      def offset(self):
          """Buffer offset (in bytes) relative to base"""
          return 0
  
      @property
      def stride(self):
          """Stride of data in memory"""
          return self._stride
  
      @property
      def size(self):
          """Number of elements in the buffer"""
          return self._size
  
      @property
      def itemsize(self):
          """The total number of bytes required to store the array data"""
          return self._itemsize
  
      @property
      def glsl_type(self):
          """GLSL declaration strings required for a variable to hold this data."""
          if self.dtype is None:
              return None
          dtshape = self.dtype[0].shape
          n = dtshape[0] if dtshape else 1
          if n > 1:
              dtype = 'vec%d' % n
          else:
              dtype = 'float' if 'f' in self.dtype[0].base.kind else 'int'
          return 'attribute', dtype
  
      def resize_bytes(self, size):
          """Resize the buffer (in-place, deferred operation)
  
          Parameters
          ----------
          size : integer
              New buffer size in bytes
  
          Notes
          -----
          This clears any pending operations.
          """
          Buffer.resize_bytes(self, size)
          self._size = size // self.itemsize
  
      def __getitem__(self, key):
          """Create a view on this buffer."""
          view = DataBufferView(self, key)
          self._views.add(view)
          return view
  
      def __setitem__(self, key, data):
          """Set data (deferred operation) """
          # If buffer has a local CPU storage, setting new values is
          # straightforward
          if self.use_cpu:
              self.cpu_data.__setitem__(key, data)
              return
  
          # Setting a whole field of the buffer: only allowed if we have CPU
          # storage. Note this case (key is string) only happen with base buffer
          if isinstance(key, str):
              raise ValueError("Cannot set non-contiguous data on buffer "
                               "that has no local CPU data.")
  
          # Setting one or several elements
          elif isinstance(key, int):
              if key < 0:
                  key += self.size
              if key < 0 or key > self.size:
                  raise IndexError("Buffer assignment index out of range")
              start, stop, step = key, key + 1, 1
          elif isinstance(key, slice):
              start, stop, step = key.indices(self.size)
              if stop < start:
                  start, stop = stop, start
          elif key == Ellipsis:
              start, stop, step = 0, self.size, 1
          else:
              raise TypeError("Buffer indices must be integers or strings")
  
          # Contiguous update?
          if step != 1:
              raise ValueError("Cannot set non-contiguous data on buffer "
                               "that has no local CPU data.")
  
          # Make sure data is an array
          if not isinstance(data, np.ndarray):
              data = np.array(data, dtype=self.dtype, copy=False)
  
          # Make sure data is big enough
          if data.size < stop - start:
              data = np.resize(data, stop - start)
          elif data.size > stop - start:
              raise ValueError('Data too big to fit GPU data '
                               '(%d > %d-%d).' % (data.size, stop, start))
  
          # Set data
          offset = start
          self.set_subdata(data=data, offset=offset, copy=True)
  
      def __repr__(self):
          return ("<%s size=%s last_dim=%s>" %
                  (self.__class__.__name__, self.size, self._last_dim))
  
  
  class DataBufferView(DataBuffer):
      """View on a sub-region of a DataBuffer.
  
      Parameters
      ----------
      base : DataBuffer
          The buffer accessed by this view.
      key : str, int, slice, or Ellpsis
          The index into the base buffer that defines a sub-region of the buffer
          to view. String arguments select a single field from multi-field
          dtypes, and other allowed types select a subset of rows.
  
      Notes
      -----
      It is generally not necessary to instantiate this class manually; use
      ``base_buffer[key]`` instead.
      """
  
      # Note that this class is a bit evil: it is a subclass of GLObject,
      # Buffer and DataBuffer, but any of these __init__'s are not called ...
  
      def __init__(self, base, key):
          # Note how this never runs the super's __init__,
          # all attributes must thus be set here ...
  
          self._base = base
          self._key = key
          self._stride = base.stride
  
          if isinstance(key, str):
              self._dtype = base.dtype[key]
              self._offset = base.dtype.fields[key][1]
              self._nbytes = base.size * self._dtype.itemsize
              self._size = base.size
              self._itemsize = self._dtype.itemsize
              return
  
          if isinstance(key, int):
              if key < 0:
                  key += base.size
              if key < 0 or key > base.size:
                  raise IndexError("Buffer assignment index out of range")
              start, stop, step = key, key + 1, 1
          elif isinstance(key, slice):
              start, stop, step = key.indices(base.size)
              if stop < start:
                  start, stop = stop, start
          elif key == Ellipsis:
              start, stop, step = 0, base.size, 1
          else:
              raise TypeError("Buffer indices must be integers or strings")
  
          if step != 1:
              raise ValueError("Cannot access non-contiguous data")
  
          self._itemsize = base.itemsize
          self._offset = start * self.itemsize
          self._size = stop - start
          self._dtype = base.dtype
          self._nbytes = self.size * self.itemsize
  
      @property
      def glir(self):
          return self._base.glir
  
      @property
      def id(self):
          return self._base.id
  
      @property
      def _last_dim(self):
          return self._base._last_dim
  
      def set_subdata(self, data, offset=0, copy=False, **kwargs):
          raise RuntimeError("Cannot set data on buffer view.")
  
      def set_data(self, data, copy=False, **kwargs):
          raise RuntimeError("Cannot set data on buffer view.")
  
      @property
      def offset(self):
          """Buffer offset (in bytes) relative to base"""
          return self._offset
  
      @property
      def base(self):
          """Buffer base if this buffer is a view on another buffer."""
          return self._base
  
      def resize_bytes(self, size):
          raise RuntimeError("Cannot resize buffer view.")
  
      def __getitem__(self, key):
          raise RuntimeError("Can only access data from a base buffer")
  
      def __setitem__(self, key, data):
          raise RuntimeError("Cannot set data on Buffer view")
  
      def __repr__(self):
          return ("<DataBufferView on %r at offset=%d size=%d>" %
                  (self.base, self.offset, self.size))
  
  
  # ------------------------------------------------------ VertexBuffer class ---
  class VertexBuffer(DataBuffer):
      """Buffer for vertex attribute data
  
      Parameters
      ----------
      data : ndarray
          Buffer data (optional)
      """
  
      _GLIR_TYPE = 'VertexBuffer'
  
      def _prepare_data(self, data, convert=False):
          # Build a structured view of the data if:
          #  -> it is not already a structured array
          #  -> shape if 1-D or last dimension is 1,2,3 or 4
          if isinstance(data, list):
              data = np.array(data, dtype=np.float32)
          if not isinstance(data, np.ndarray):
              raise ValueError('Data must be a ndarray (got %s)' % type(data))
          if data.dtype.isbuiltin:
              if convert is True:
                  data = data.astype(np.float32)
              if data.dtype in (np.float64, np.int64):
                  raise TypeError('data must be 32-bit not %s'
                                  % data.dtype)
              c = data.shape[-1] if data.ndim > 1 else 1
              if c in [2, 3, 4]:
                  if not data.flags['C_CONTIGUOUS']:
                      logger.warning('Copying discontiguous data for struct '
                                     'dtype:\n%s' % _last_stack_str())
                      data = data.copy()
              else:
                  c = 1
              if self._last_dim and c != self._last_dim:
                  raise ValueError('Last dimension should be %s not %s'
                                   % (self._last_dim, c))
              dtype_def = ('f0', data.dtype.base)
              if c != 1:
                  # numpy dtypes with size 1 are ambiguous, only add size if it is greater than 1
                  dtype_def += (c,)
              data = data.view(dtype=[dtype_def])
              self._last_dim = c
          return data
  
  
  def _last_stack_str():
      """Print stack trace from call that didn't originate from here"""
      stack = extract_stack()
      for s in stack[::-1]:
          if op.join('vispy', 'gloo', 'buffer.py') not in __file__:
              break
      return format_list([s])[0]
  
  
  # ------------------------------------------------------- IndexBuffer class ---
  class IndexBuffer(DataBuffer):
      """Buffer for index data
  
      Parameters
      ----------
      data : ndarray | None
          Buffer data.
      """
  
      _GLIR_TYPE = 'IndexBuffer'
  
      def __init__(self, data=None):
          DataBuffer.__init__(self, data)
          self._last_dim = 1
  
      def _prepare_data(self, data, convert=False):
          if isinstance(data, list):
              data = np.array(data, dtype=np.uint32)
          if not isinstance(data, np.ndarray):
              raise ValueError('Data must be a ndarray (got %s)' % type(data))
          if not data.dtype.isbuiltin:
              raise TypeError("Element buffer dtype cannot be structured")
          else:
              if convert:
                  if data.dtype is not np.uint32:
                      data = data.astype(np.uint32)
              else:
                  if data.dtype not in [np.uint32, np.uint16, np.uint8]:
                      raise TypeError("Invalid dtype for IndexBuffer: %r" %
                                      data.dtype)
          return data
  