# -*- coding: utf-8 -*-
# Copyright (C) 2012, Almar Klein
#
# Visvis is distributed under the terms of the (new) BSD License.
# The full license can be found in 'license.txt'.

""" Module cameras

A camera represents both the camera model and the interaction style.

A lot of blood sweat and tears went into this module to get the
OpenGL transformations right and calculate screen to world coordinates
etc. Please be carefull when changing stuff here.

The models were designed such that the order of transformations makes 
sense, and that as much as possible "just works". Also the diffent
models were designed to be as consistent as possible.

"""

import OpenGL.GL as gl
import OpenGL.GLU as glu

import math

import visvis as vv
from visvis.core.misc import Property, Range, basestring
from visvis.core import constants
from visvis.utils.pypoints import Quaternion, Point

# Global to store depth Bits
depthBits = [0]
# Trig functions in degrees
sind = lambda q: math.sin(q*math.pi/180)
cosd = lambda q: math.cos(q*math.pi/180)

# The cmp function is gone in Py3k
cmp = lambda a,b: (a > b) - (a < b)


def getDepthValue():
    """ Get the depth value.
    
    for glOrto(x1,y1,x2,y2,n,f) and s the depth buffer depth:
    def calcPrecision(z=0, n=1000, bits=16): # approximates precision
        z+=n
        return z * z / ( n * float(1<<bits) - z )
        
    For 24 bits and more, we're fine with 100.000, but for 16 bits we
    need 3000 or so. The criterion is that at the center, we should be
    able to distinguish between 0.1, 0.0 and -0.1 etc. So we can draw lines
    on top (0.1) then the gridlines (0.0) and then 2d textures 
    (0.1, 0.2, etc.).
    
    """
    if not depthBits[0]:
        bits = gl.glGetInteger(gl.GL_DEPTH_BITS)
        if bits:
            depthBits[0] = bits
    # Process
    if depthBits[0] < 24:
        return 3000
    else:
        return 100000


def ortho(x1, x2, y1,y2):
    """ ortho(x1, x2, y1, y2)
    
    Like gl.glOrtho() but the z-values are automatically determined
    dependent on the amount of bits in the depth buffer.
    
    """    
    val = getDepthValue()
    gl.glOrtho(x1, x2, y1, y2, -val, val)


def depthToZ(depth):
    """ depthToZ(depth)
    
    Get the z-coord, given the depth value. 
    
    """
    val = getDepthValue()
    return val - depth * 2 * val


class BaseCamera(object):
    """ BaseCamera()
    
    Abstract camera class. A camera represents both the camera model
    and the interaction style.
    
    """
    
    # The names by which this camera type is known
    _NAMES = ()
    
    # Subclasses should set viewparams; the list if attributes that define
    # the view of the camera.
    _viewparams = ('daspect', 'loc', 'zoom')
    
    def __init__(self):
        
        # Init list of axeses that this camera applies to
        self._axeses = []
        
        # Init limits of what to visualize        
        self._xlim = Range(0,1)
        self._ylim = Range(0,1)
        self._zlim = Range(0,1)
        
        # Init view location
        self._view_loc = 0,0,0
        
        # Init zoom factor
        self._zoom = 1.0
        
        # Init daspect
        self._daspect = 1.0, 1.0, 1.0
        
        # Variable to keep track of window size during resizing
        self._windowSizeFactor = 0
    
    
    def _RegisterAxes(self, axes):
        """ _RegisterAxes(axes)
        
        Method used by the axes to register itself at this camera.
        
        """
        
        # Make sure it not currently registered
        self._UnregisterAxes(axes)
        
        # Append to list
        self._axeses.append(axes)
        
        # Bind to events
        axes.eventPosition.Bind(self.OnResize)
        axes.eventKeyDown.Bind(self.OnKeyDown)
        axes.eventKeyUp.Bind(self.OnKeyUp)        
        # 
        axes.eventMouseDown.Bind(self.OnMouseDown)
        axes.eventMouseUp.Bind(self.OnMouseUp)        
        axes.eventMotion.Bind(self.OnMotion)
        axes.eventDoubleClick.Bind(self.OnDoubleClick)
    
    
    def _UnregisterAxes(self, axes):
        """ _UnregisterAxes(axes)
        
        Method used by the axes to unregister itself at this camera.
        
        """
        
        # Remove from list
        while axes in self._axeses:
            self._axeses.remove(axes)
        
        # Unbind events
        axes.eventPosition.Unbind(self.OnResize)
        axes.eventKeyDown.Unbind(self.OnKeyDown)
        axes.eventKeyUp.Unbind(self.OnKeyUp)        
        # 
        axes.eventMouseDown.Unbind(self.OnMouseDown)
        axes.eventMouseUp.Unbind(self.OnMouseUp)        
        axes.eventMotion.Unbind(self.OnMotion)
        axes.eventDoubleClick.Unbind(self.OnDoubleClick)
    
    
    def OnResize(self, event):
        pass
    def OnKeyDown(self, event):
        pass
    def OnKeyUp(self, event):
        pass
    def OnMouseDown(self, event):
        pass
    def OnMouseUp(self, event):
        pass
    def OnMotion(self, event):
        pass
    
    def OnDoubleClick(self, event):
        # Reset view if this is the current camera.
        if self is self.axes.camera:
            return self.Reset()
    
    @property
    def axes(self):
        """ Get the axes that this camera applies to (or the first axes if
        it applies to multiple axes).
        """
        if self._axeses:
            return self._axeses[0]
        else:
            return None
    
    @property
    def axeses(self):
        """ Get a tuple with the axeses that this camera applies to.
        """
        return tuple(self._axeses)
    
    
    @Property
    def daspect():
        """ Get/Set the data aspect ratio as a three element tuple. 
        
        The daspect is a 3-element tuple (x,y,z). If a 2-element tuple is
        given, z is assumed 1. Note that only the ratio between the values
        matters (i.e. (1,1,1) equals (2,2,2)). When a value is negative, the 
        corresponding dimension is flipped. 
        
        Note that if axes.daspectAuto is True, the daspect is changed by the 
        camera to nicely scale the data to fit the screen (but the sign
        is preserved).
        """
        def fget(self):            
            return self._daspect
        def fset(self, value):
            try:
                l = len(value)
            except TypeError:
                raise Exception("You can only set daspect with a sequence.")
            if 0 in value:
                raise Exception("The given daspect should not contain zeros.")
            if l==2:            
                self._daspect = (float(value[0]), float(value[1]), 1.0)
            elif l==3:
                self._daspect = tuple([float(v) for v in value])
            else:            
                raise Exception("daspect should be a length 2 or 3 sequence!")
        return locals()
    
    
    @property
    def daspectNormalized(self):

        """ Get the data aspect ratio, normalized such that the x scaling 
        is +/- 1.
        """
        return tuple(d/abs(self._daspect[0]) for d in self._daspect)
    
    @Property
    def zoom():
        """ Get/set the current zoom factor.
        """
        def fget(self):
            return self._zoom
        def fset(self, value):
            self._zoom = float(value)
            for 
ax in self.axeses:
                ax.Draw()
        return locals()
    
    @Property
    def loc():
        """ Get/set the current viewing location.
        """
        def fget(self):
            return tuple(self._view_loc)
        def fset(self, value):
            # Check
            if isinstance(value, (tuple, list)) and len(value)==3:
                value = [float(v) for v in value]
            else:
                raise ValueError('loc must be a 3-element tuple.')
            # Set
            self._view_loc = tuple(value)
            for ax in self.axeses:
                ax.Draw()
        return locals()
    
    
    def GetLimits(self):
        """ GetLimits()
        
        Return the extent of the axes, which is determined by the camera
        in the 2D case.
        
        """
        return self._xli