# Soya 3D
# Copyright (C) 2004 Jean-Baptiste LAMY -- jiba@tuxfamily.org
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA

cdef class _Portal(CoordSyst):
  #cdef _World  _beyond
  #cdef         _beyond_name
  #cdef double* _equation  # Clip plane equations
  #cdef Context _context
  
  #cdef int     _nb_vertices # NB vertex and their coordinates for the portal quad ;
  #cdef float*  _coords      # used to draw this quad (for bounds atmosphere or teleporter).
  
  property beyond:
    def __get__(self):
      return self._beyond
    def __set__(self, _World beyond):
      self._beyond = beyond
      if beyond is None: self._beyond_name = ""
      else:              self._beyond_name = beyond._filename
      
  property beyond_name:
    def __get__(self):
      return self._beyond_name
    def __set__(self, name):
      self._beyond_name = name
      
  property nb_clip_planes:
    def __get__(self):
      if self._option & PORTAL_USE_4_CLIP_PLANES: return 4
      if self._option & PORTAL_USE_5_CLIP_PLANES: return 5
      return 0
    def __set__(self, int x):
      if   x == 4:
        self._option   = self._option |  PORTAL_USE_4_CLIP_PLANES
        self._option   = self._option & ~PORTAL_USE_5_CLIP_PLANES
        self._equation = <double*> realloc(self._equation, 16 * sizeof(double))
      elif x == 5:
        self._option   = self._option & ~PORTAL_USE_4_CLIP_PLANES
        self._option   = self._option |  PORTAL_USE_5_CLIP_PLANES
        self._equation = <double*> realloc(self._equation, 20 * sizeof(double))
      else:
        self._option   = self._option & ~PORTAL_USE_4_CLIP_PLANES
        self._option   = self._option & ~PORTAL_USE_5_CLIP_PLANES
        free(self._equation)
        self._equation = NULL
        
#   property infinite:
#     def __get__(self):
#       return self._option & PORTAL_INFINITE
#     def __set__(self, int x):
#       if x: self._option = self._option |  PORTAL_INFINITE
#       else: self._option = self._option & ~PORTAL_INFINITE
      
  property bound_atmosphere:
    def __get__(self):
      return self._option & PORTAL_BOUND_ATMOSPHERE
    def __set__(self, int x):
      if x: self._option = self._option |  PORTAL_BOUND_ATMOSPHERE
      else: self._option = self._option & ~PORTAL_BOUND_ATMOSPHERE

  def __init__(self, _World parent = None):
    CoordSyst.__init__(self, parent)
    self._option   = self._option | PORTAL_USE_4_CLIP_PLANES | PORTAL_BOUND_ATMOSPHERE
    self._equation = <double*> malloc(16 * sizeof(double))
    
  def load_beyond(self):
    """Loads and returns the World beyond the portal.
The default implementation call World.get(self.beyond_name), but you can overrides
it, e.g. if you want to create the world beyond from scratch."""
    if self.beyond_name: return World.get(self.beyond_name)
    return None
  
  def unload_beyond(self):
    """Called when the portal is no longer visible.
The default implementation does nothing, but you can override it, e.g. to remove
portal.beyond from memory (with 'portal.beyond = None')."""
    pass
  
  
  cdef void _compute_clipping_planes(self):
    cdef float* m
    # we must put these points and vectors in the camera coordsys
    cdef float p1[3], p2[3], v1[3], v2[3]
    p1[0] = p1[1] = -0.5
    p1[2] = 0.0
    p2[0] = p2[1] = 0.5
    p2[2] = 0.0
    v1[0] = v1[2] = 0.0
    v1[1] = 1.0
    v2[1] = v2[2] = 0.0
    v2[0] = 1.0
    
    m = self._root_matrix()
    point_by_matrix (p1, m)
    point_by_matrix (p2, m)
    vector_by_matrix(v1, m)
    vector_by_matrix(v2, m)
    
    m = renderer.current_camera._inverted_root_matrix()
    point_by_matrix (p1, m)
    point_by_matrix (p2, m)
    vector_by_matrix(v1, m)
    vector_by_matrix(v2, m)
    
    # compute planes equations
    self._equation[ 0] = <double> ( p1[1] * v1[2] - p1[2] * v1[1])
    self._equation[ 1] = <double> (-p1[0] * v1[2] + p1[2] * v1[0])
    self._equation[ 2] = <double> ( p1[0] * v1[1] - p1[1] * v1[0])
    self._equation[ 3] = <double> ( 0.0)
    self._equation[ 4] = <double> (-p2[1] * v1[2] + p2[2] * v1[1])
    self._equation[ 5] = <double> ( p2[0] * v1[2] - p2[2] * v1[0])
    self._equation[ 6] = <double> (-p2[0] * v1[1] + p2[1] * v1[0])
    self._equation[ 7] = <double> ( 0.0)
    self._equation[ 8] = <double> (-p1[1] * v2[2] + p1[2] * v2[1])
    self._equation[ 9] = <double> ( p1[0] * v2[2] - p1[2] * v2[0])
    self._equation[10] = <double> (-p1[0] * v2[1] + p1[1] * v2[0])
    self._equation[11] = <double> ( 0.0)
    self._equation[12] = <double> ( p2[1] * v2[2] - p2[2] * v2[1])
    self._equation[13] = <double> (-p2[0] * v2[2] + p2[2] * v2[0])
    self._equation[14] = <double> ( p2[0] * v2[1] - p2[1] * v2[0])
    self._equation[15] = <double> ( 0.0)
    if self._option & PORTAL_USE_5_CLIP_PLANES:
      self._equation[16] = <double> ( v1[1] * v2[2] - v1[2] * v2[1])
      self._equation[17] = <double> (-v1[0] * v2[2] + v1[2] * v2[0])
      self._equation[18] = <double> ( v1[0] * v2[1] - v1[1] * v2[0])
      self._equation[19] = <double> -(self._equation[16] * p1[0] + self._equation[17] * p1[1] + self._equation[18] * p1[2])
      
  cdef void _compute_points(self):
    cdef int      nb, i
    cdef float    v, f
    cdef float*   p1, *p2
    cdef Frustum* frustum
    # it's difficult to draw the portal quad even if a part of the quad is below the camera
    # front plane (we must avoid blinking when the camera is very near the portal)
    
    cdef float p[12] # = { -0.5, -0.5, 0.0, 0.5, -0.5, 0.0, 0.5, 0.5, 0.0, -0.5, 0.5, 0.0 }
    p[ 0] = p[ 1] = -0.5
    p[ 2] =  0.0
    p[ 3] =  0.5
    p[ 4] = -0.5
    p[ 5] =  0.0
    p[ 6] = p[ 7] = 0.5
    p[ 8] =  0.0
    p[ 9] = -0.5
    p[10] =  0.5
    p[11] =  0.0
    
    # put the 4 points of the portal in the camera coordsys. render_matrix must be set !!!
    point_by_matrix(p,     self._render_matrix)
    point_by_matrix(p + 3, self._render_matrix)
    point_by_matrix(p + 6, self._render_matrix)
    point_by_matrix(p + 9, self._render_matrix)
    
    # find any point is behind the camera front plane
    free(self._coords)
    if (p[2] > -renderer.current_camera._front) or (p[5] > -renderer.current_camera._front) or (p[8] > -renderer.current_camera._front) or (p[11] > -renderer.current_camera._front):
      # intersect portal with front plane
      frustum = renderer.current_camera._frustum
      p2 = <float*> malloc(4 * sizeof(float))
      p2[0] = -frustum.planes[0]
      p2[1] = -frustum.planes[1]
      p2[2] = -frustum.planes[2]
      p2[3] =  frustum.planes[3]
      face_intersect_plane(p, 4, p2, &p1, &nb)
      free(p2)
      # intersect portal with side planes
      face_intersect_plane(p1, nb, frustum.planes +  4, &p2, &nb); free(p1)
      face_intersect_plane(p2, nb, frustum.planes +  8, &p1, &nb); free(p2)
      face_intersect_plane(p1, nb, frustum.planes + 12, &p2, &nb); free(p1)
      face_intersect_plane(p2, nb, frustum.planes + 16, &self._coords, &(self._nb_vertices))
      free(p2)
      # push portal points to the front plane ;
      # actually we must not draw the vertices with the camera front value as Z coordinate,
      # because some OpenGL won't draw the quad
      
      p1 = self._coords
      f = -(renderer.current_camera._front + renderer.current_camera._back) * 0.5
      i = 0
      while i < 3 * self._nb_vertices:
        v = f / p1[i + 2]
        p1[i]     = v * p1[i]
        p1[i + 1] = v * p1[i + 1]
        p1[i + 2] = f
        i = i + 3
    else:
      self._coords      = NULL
      self._nb_vertices = 0
    
    self._coords = <float*> realloc(self._coords, (self._nb_vertices + 4) * 3 * sizeof(float))
    nb = self._nb_vertices * 3
    memcpy(self._coords + nb, p, 12 * sizeof(float))
    
  cdef void _batch(self, CoordSyst coordsyst):
    cdef int      i
    cdef float    p
    cdef float    sphere[4]
    cdef float*   matrix, *ptr
    cdef Frustum* f
    cdef Context  ctx

    if self._option & HIDDEN: return
    self._frustum_id = -1
    
    # visibility test : culling
    ptr    = renderer.root_frustum.position
    matrix = self._inverted_root_matrix()
    p      = ptr[0] * matrix[2] + ptr[1] * matrix[6] + ptr[2] * matrix[10] + matrix[14]
    if p < 0.0: # camera is behind the portal => nothing to render
      if not self._beyond is None: self.unload_beyond()
      return
    
    # visibility test : frustum
    #if self._option & PORTAL_INFINITE:
    if self._option & (PORTAL_USE_4_CLIP_PLANES | PORTAL_USE_5_CLIP_PLANES):
      f = renderer._frustum(self._parent)
      sphere[0] = self._matrix[12]
      sphere[1] = self._matrix[13]
      sphere[2] = self._matrix[14]
      if self._matrix[16] > self._matrix[17]: sphere[3] = sqrt_2 * self._matrix[16]
      else:                                   sphere[3] = sqrt_2 * self._matrix[17]
      if sphere_in_frustum(f, sphere) == 0:
        if not self._beyond is None: self.unload_beyond()
        return
    else:
      f = renderer._frustum(self)
      i = 2
      while i < 24:
        if f.points[i] <= 0.0:
          if not self._beyond is None: self.unload_beyond()
          return
        i = i + 3
      
    # load world self._beyond if necessary
    if self._beyond is None:
      self._beyond = self.load_beyond()
      if self._beyond is None: return
      
    # determine clipping planes
    if self._equation != NULL: self._compute_clipping_planes()
    if self._option & PORTAL_BOUND_ATMOSPHERE:# || self._option & PORTAL_TELEPORTER
      # compute render matrix to go to the portal
      #multiply_matrix(self._render_matrix, renderer.current_camera._render_matrix, self._root_matrix())
      multiply_matrix(self._render_matrix, coordsyst._render_matrix, self._matrix)
      self._compute_points()
      
    renderer.portals.append(self)
    # avoid batching the same world twice
    if self._beyond._option & WORLD_BATCHED: return
    if renderer.current_camera.to_render is None:
      if renderer.current_camera._get_root()._contains(self._beyond): return
    elif renderer.current_camera._to_render ._contains(self._beyond): return
    
    # create a new context
    ctx = renderer.current_context
    self._context = renderer._context()
    self._context.atmosphere = self._beyond._atmosphere
    self._context.portal     = self
    renderer.current_context = self._context
    self._beyond._option = self._beyond._option | WORLD_BATCHED # mark the world self._beyond as batched
    renderer.worlds_made.append(self._beyond)
    multiply_matrix(self._beyond._render_matrix, renderer.current_camera._render_matrix, self._beyond._root_matrix())
    self._beyond._batch(None)
    renderer.current_context = ctx
    
  cdef int _shadow(self, CoordSyst coordsyst, _Light light):
    if (light._option & LIGHT_TOP_LEVEL) and (not self._beyond is None):
      return self._beyond._shadow(coordsyst, light)
    return 0
  
  cdef void _atmosphere_clear_part(self):
    cdef int         i
    cdef float*      ptr
    cdef _Atmosphere atmosphere
    atmosphere = self._beyond._atmosphere
    
    # draw the part of the portal that is in front of the camera
    glLoadIdentity()
    glDisable(GL_TEXTURE_2D)
    glDisable(GL_FOG)
    glDisable(GL_LIGHTING)
    glDepthMask(GL_FALSE)
    glColor4fv(atmosphere._bg_color)
    glDisable(GL_CULL_FACE)
    ptr = self._coords + (self._nb_vertices * 3)
    glBegin(GL_QUADS)
    glVertex3fv(ptr)
    glVertex3fv(ptr + 3)
    glVertex3fv(ptr + 6)
    glVertex3fv(ptr + 9)
    glEnd()
    
    # draw the part of the portal that is behind the front plane of the camera
    if self._nb_vertices > 0:
      glBegin(GL_POLYGON)
      i = 0
      while i < self._nb_vertices * 3:
        glVertex3fv(self._coords + i)
        i = i + 3
      glEnd()
      
    # skyplane
    if isinstance(atmosphere, _SkyAtmosphere):
      # activate and set up clip planes
      if self._equation == NULL:
        self._equation = <double*> malloc(16 * sizeof(double))
        self._compute_clipping_planes()
      glClipPlane(GL_CLIP_PLANE0, self._equation)
      glClipPlane(GL_CLIP_PLANE1, self._equation + 4)
      glClipPlane(GL_CLIP_PLANE2, self._equation + 8)
      glClipPlane(GL_CLIP_PLANE3, self._equation + 12)
      glEnable(GL_CLIP_PLANE0)
      glEnable(GL_CLIP_PLANE1)
      glEnable(GL_CLIP_PLANE2)
      glEnable(GL_CLIP_PLANE3)
      atmosphere._draw_bg() # draw sky
      glDisable(GL_CLIP_PLANE0)
      glDisable(GL_CLIP_PLANE1)
      glDisable(GL_CLIP_PLANE2)
      glDisable(GL_CLIP_PLANE3)
      
    glEnable(GL_CULL_FACE)
    glDepthMask(GL_TRUE)
    glEnable(GL_TEXTURE_2D)
    glEnable(GL_FOG)
    glEnable(GL_LIGHTING)
    

  cdef void _draw_fog(self, _Atmosphere atmosphere):
    cdef float* ptr
    
    # draw the part of the portal that is in front of the camera
    glDisable(GL_TEXTURE_2D)
    glDisable(GL_FOG)
    glDisable(GL_LIGHTING)
    # glDepthMask(GL_FALSE)
    glDisable(GL_CULL_FACE)
    # glEnable(GL_BLEND)
    glLoadIdentity()
    ptr = self._coords + (self._nb_vertices * 3)
    glBegin(GL_QUADS)
    glColor4f(atmosphere._fog_color[0], atmosphere._fog_color[1], atmosphere._fog_color[2], atmosphere._fog_factor_at(ptr    )); glVertex3fv(ptr    )
    glColor4f(atmosphere._fog_color[0], atmosphere._fog_color[1], atmosphere._fog_color[2], atmosphere._fog_factor_at(ptr + 3)); glVertex3fv(ptr + 3)
    glColor4f(atmosphere._fog_color[0], atmosphere._fog_color[1], atmosphere._fog_color[2], atmosphere._fog_factor_at(ptr + 6)); glVertex3fv(ptr + 6)
    glColor4f(atmosphere._fog_color[0], atmosphere._fog_color[1], atmosphere._fog_color[2], atmosphere._fog_factor_at(ptr + 9)); glVertex3fv(ptr + 9)
    glEnd()
    # draw the part of the portal that is behind the front plane of the camera
    # HACK : we supposed to be too near to have fog
    # we suppose that when the protal cut the front plane of the camera, the portal is
    # near to the camera and so the fog has no influence on it
    # => we don't need to draw anything for the part of the portal that is behind
    # the front plane of the camera
    # glDisable(GL_BLEND)
    glEnable(GL_CULL_FACE)
    # glDepthMask(GL_TRUE)
    glEnable(GL_TEXTURE_2D)
    glEnable(GL_FOG)
    glEnable(GL_LIGHTING)

  cdef void _raypick(self, RaypickData raypick_data, CoordSyst raypickable):
    self._beyond._raypick(raypick_data, raypickable)
    
  cdef int _raypick_b(self, RaypickData raypick_data, CoordSyst raypickable):
    return self._beyond._raypick_b(raypick_data, raypickable)
  
  cdef void _collect_raypickables(self, Chunk* items, float* rsphere, float* sphere):
    if self._option & OBJECT_NON_SOLID: return

    cdef CoordSyst child
    cdef float* matrix
    cdef float  s[4]
    # transform sphere to my coordsys
    # XXX avoid using self._inverted_root_matrix() -- use rather the parent's result (=sphere ?) (faster)
    matrix = self._inverted_root_matrix()
    point_by_matrix_copy(s, rsphere, matrix)
    s[3] = length_by_matrix(rsphere[3], matrix)
    
    if vector_length(s) < s[3] + 0.5: # Else, it is too far
      self._beyond._collect_raypickables(items, rsphere, sphere)
    
    
  def has_passed_through(self, Position old_pos, Position new_pos):
    """Portal.has_passed_though(self, old_pos, new_pos) -> bool

Return true if moving from OLD_POS to NEW_POS pass through the portal.
NEW_POS can be either the new position or the speed vector."""
    if self._beyond is None: return 0
    
    cdef float old[3], new[3]
    cdef float f, f1, f2
    old_pos._into(self, old)
    new_pos._into(self, new)
    if isinstance(new_pos, _Vector):
      new[0] = old[0] + new[0]
      new[1] = old[1] + new[1]
      new[2] = old[2] + new[2]
      
    f = old[2] * new[2]
    if  f  > 0.0: return 0 # does not pass through the plane
    if (f == 0.0) and (old[2] == 0.0): return 0 # pass by Z=0.0
    f1 = old[2] / (old[2] + new[2])
    f2 = 1.0 - f1
    return (-0.5 < old[0] * f1 + new[0] * f2 < 0.5) and (-0.5 < old[1] * f1 + new[1] * f2 < 0.5)
  
  # Implemented in Python, due to the use of lambda
  # def pass_through(self, CoordSyst coordsyst):
  
  cdef __getcstate__(self):
    #return self._parent, self._option, self._matrix[0], self._matrix[1], self._matrix[2], self._matrix[3], self._matrix[4], self._matrix[5], self._matrix[6], self._matrix[7], self._matrix[8], self._matrix[9], self._matrix[10], self._matrix[11], self._matrix[12], self._matrix[13], self._matrix[14], self._matrix[15], self._matrix[16], self._matrix[17], self._matrix[18], self._beyond_name
    return CoordSyst.__getcstate__(self), self._beyond_name
  cdef void __setcstate__(self, cstate):
    #self._parent, self._option, self._matrix[0], self._matrix[1], self._matrix[2], self._matrix[3], self._matrix[4], self._matrix[5], self._matrix[6], self._matrix[7], self._matrix[8], self._matrix[9], self._matrix[10], self._matrix[11], self._matrix[12], self._matrix[13], self._matrix[14], self._matrix[15], self._matrix[16], self._matrix[17], self._matrix[18], self._beyond_name = cstate
    CoordSyst.__setcstate__(self, cstate[0])
    self._beyond_name = cstate[1]
    


