# Soya 3D
# Copyright (C) 2001-2002 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

"""soya.cal3d

Cal3D (http://cal3d.sourceforge.net) is a library for 3D bone-based animations,
designed for character animation. It supports clothes and hair, mixing several
animations, dismembering, ...
This module allows to use Cal3D and Cal3D files in Soya; it contains Shape and
Volume classes for displaying Cal3D animated objects.

The following Cal3D features are not implemented yet:
 - multiple texturing
 - material thread
 - material color (see below)

Cal3D uses its own file format, and not the Soya one. See Cal3D documentation
about how to produce Cal3D file. There is not a lot of Free Software solution
available yet, though.
Soya can deal with these files about like any other Soya file: you can put in your
shape directory a subdirectory with Cal3D files (skeleton, animations, meshes,
materials), and a ".cfg" file, and then use cal3d.Shape.get(...).

About materials: when loading a material file, Soya first checks if there is a Soya
material with the same name that the material texture filename (without extention).
If so, Soya will use this material instead of the one provided by Cal3D.
Else, Soya will create a new Soya material from the Cal3D material file. Textures
format suported are PNG, TGA and the ".raw" used by Cal3D examples. Material colors
are *NOT* read from Cal3D material file.

See tutorial lesson 014 for example."""

import sys, os, os.path, weakref
import soya, _soya, soya.model as model, soya.soya3d as soya3d

class Shape(soya.SavedInAPath, soya._CObj, _soya._Cal3DShape):
  """Shape

A Cal3D shape (do not counfound with model.Shape!) wraps a Cal3D CoreModel.

The meshes and animations attributes contains the lists of meshes and animations
names available for the shape.

To load a Cal3D shape, use cal3d.Shape.load() or cal3d.parse_cfg_file()."""
  
  _alls = weakref.WeakValueDictionary() # Required by SavedInAPath
  PATH = ""
  def __init__(self):
    soya.SavedInAPath.__init__(self, "")
    _soya._Cal3DShape.__init__(self)
    
    self.meshes     = []
    self.animations = []
    
  def _get_material_4_cal3d(self, image_filename, diffuse_r, diffuse_g, diffuse_b, diffuse_a, specular_r, specular_g, specular_b, specular_a, shininess):
    try:
      material_name = os.path.basename(image_filename)
      material_name = material_name[:material_name.find(".")]
      if material_name and os.path.exists(os.path.join(model.Material.PATH, material_name + ".data")): # A Soya material corresponds => use it
        return model.Material.get(material_name)
      
      else: # Not a Soya material
        material = model.Material()
        material.diffuse   = (diffuse_r , diffuse_g , diffuse_b , diffuse_a )
        material.specular  = (specular_r, specular_g, specular_b, specular_a)
        material.shininess = shininess
        if image_filename:
          if  image_filename.endswith(".png") or image_filename.endswith(".tga"):
            material.image = _soya._Image(os.path.join(os.path.dirname(self.full_filename), image_filename))
          elif image_filename.endswith(".raw"):
            material.image = load_raw_image(os.path.join(os.path.dirname(self.full_filename), image_filename))
          else: print "Warning: unsupported image format:", image_filename
          
        return material
    except: # As this method is called by C code, exception may not be dumped.
      sys.excepthook(*sys.exc_info())
      
  _alls      = model.Shape._alls
  get        = model.Shape.get
  def availables(klass):
    """Shape.availables()

Returns the list of availables Cal3D shape in model.Shape.PATH."""
    return filter(lambda path: os.path.isdir(os.path.join(self.PATH, path)) and os.path.exists(os.path.join(self.PATH, path, path + ".cfg")), os.listdir(self.PATH))
#    return filter(lambda path: os.path.isdir(os.path.join(model.Shape.PATH, path)) and os.path.exists(os.path.join(model.Shape.PATH, path, path + ".cfg")), os.listdir(model.Shape.PATH))
  availables = classmethod(availables)
  
  def save(self, filename = None): raise NotImplementedError()
  def get(klass, name): return Shape._alls.get(name) or Shape.load(name)
  get = classmethod(get)
  def load(klass, filename):
    shape = parse_cfg_file(os.path.join(klass.PATH or model.Shape.PATH, filename, filename + ".cfg"))
    klass._alls[filename] = shape
    return shape
  load = classmethod(load)
  
MAT = []
  
def load_raw_image(filename):
  """Loads a ".raw" image file, which are used by Cal3D example (see cal3d_data).
Returns a Soya image object, suitable for model.material.image."""
  import struct, array
  
  f = open(filename)
  width, height, nb_colors = struct.unpack("iii", f.read(12))
  data = array.array("c", f.read())
  
  # Flip texture around y-axis (-> opengl-style).
  data2 = array.array("c", " " * len(data))
  line_length = width * nb_colors
  for y in range(height):
    data2[y * line_length : (y + 1) * line_length] = data[(height - y - 1) * line_length : (height - y) * line_length]
    
  return _soya._Image(data2.tostring(), width, height, nb_colors)

def parse_cfg_file(filename):
  """Reads a the Cal3D .cfg file, and creates and returns a Cal3D shape from it."""
  shape   = Shape()
  dirname = os.path.dirname(filename)
  shape._filename = os.path.basename(dirname)
  shape.full_filename = filename
  
  for line in open(filename).readlines():
    if line and (line[0] != "#"):
      parts = line.split("=")
      if len(parts) == 2:
        key, value = parts
        value = value.rstrip()
        if   key == "skeleton":
          shape._load_skeleton(os.path.join(dirname, value))
        elif key == "mesh":
          shape._load_mesh(os.path.join(dirname, value))
          shape.meshes.append(os.path.basename(value)[:-4])
        elif key == "material":
          shape._load_material(os.path.join(dirname, value))
        elif key == "animation":
          shape._load_animation(os.path.join(dirname, value))
          shape.animations.append(os.path.basename(value)[:-4])
        else: print "Warning: unknows Cal3D .cfg tag:   %s=%s" % (key, value)
        
  shape._build_materials()
  
  return shape


class Volume(soya._CObj, soya3d.GraphicElement, _soya._Cal3DVolume):
  """Volume

A Cal3D volume (do not counfound with soya3d.Volume!) wraps a Cal3D Model.
It displays a Cal3D shape. See Volume.animate* methods in order to animate it."""
  def __init__(self, parent = None, shape = None, attached_meshes = None, name = ""):
    """Volume(parent = None, shape = None, visible_meshes = None, name = "")

Creates a new Cal3D volume. PARENT is the volume's parent, and SHAPE is the Cal3D
shape (you cannot use "normal" Soya's shape here !).

ATTACHED_MESHES is a list of meshes names to attach; if ATTACHED_MESHES is None,
all meshes are attached. See volume.shape.meshes to get the list of available
mesh names."""
    
    _soya._Cal3DVolume.__init__(self)
    self.name = name
    
    if shape : self.set_shape(shape)
    if parent: parent.add(self)
    if attached_meshes:
      for attached_mesh in attached_meshes:
        self._set_attached(shape.meshes.index(attached_mesh), 1)
      self._build_subshapes()
    else: self.attach_all()
    
  def attach(self, *mesh_names):
    """Volume.attach(mesh_name_1, ...)

Attaches new meshes named MESH_NAME_1, ... to the volume.
See volume.shape.meshes to get the list of available mesh names.
Attaching several meshes at the same time can be faster."""
    for mesh_name in mesh_names:
      self._set_attached(self.shape.meshes.index(mesh_name), 1)
    self._build_subshapes()
    
  def detach(self, *mesh_names):
    """Volume.detach(mesh_name_1, ...)

Detaches meshes named MESH_NAME_1, ... to the volume.
Detaching several meshes at the same time can be faster."""
    for mesh_name in mesh_names:
      self._set_attached(self.shape.meshes.index(mesh_name), 0)
    self._build_subshapes()
    
  def is_attached(self, mesh_name):
    """Volume.is_attached(mesh_name)

Checks if the mesh called MESH_NAME is attached to the volume."""
    return self._is_attached(self.shape.meshes.index(mesh_name))
    
  def __repr__(self):
    if self.shape:
      if self.name: return "<cal3d.Volume %s, shape %s>" % (self.name, repr(self.shape))
      return "<cal3d.Volume, shape %s>" % repr(self.shape)
    else:
      if self.name: return "<cal3d.Volume %s, no shape>" % self.name
      return "<cal3d.Volume, no shape>"
  
  def advance_time(self, proportion):
    self._update(proportion * 0.03)
    
  def animate_blend_cycle(self, animation_name, weight = 1.0, fade_in = 0.2):
    """Volume.animate_blend_cycle(animation_name, weight = 1.0, fade_in = 0.2)

Plays animation ANIMATION_NAME in cycle.
See volume.shape.animations for the list of available animations.
WEIGHT is the weight of the animation (usefull is several animations are played
simultaneously), and FADE_IN is the time (in second) needed to reach this weight
(in order to avoid a brutal transition)."""
    self._animate_blend_cycle(self.shape.animations.index(animation_name), weight, fade_in)
    
  def animate_clear_cycle(self, animation_name, fade_out = 0.2):
    """Volume.animate_clear_cycle(animation_name, fade_out = 0.2)

Stops playing animation ANIMATION_NAME in cycle.
FADE_OUT is the time (in second) needed to stop the animation (in order to avoid
a brutal transition)."""
    self._animate_clear_cycle(self.shape.animations.index(animation_name), fade_out)
    
  def animate_execute_action(self, animation_name, fade_in = 0.2, fade_out = 0.2):
    """Volume.animate_execute_action(animation_name, fade_in = 0.2, fade_out = 0.2)

Plays animation ANIMATION_NAME once.
See volume.shape.animations for the list of available animations.
FADE_IN and FADE_OUT are the time (in second) needed to reach full weight, and to
stop the animation (in order to avoid brutal transitions)."""
    self._animate_execute_action(self.shape.animations.index(animation_name), fade_in, fade_out)

  def attach_to_bone(self, coordsys, bone_name):
    """Volume.attach_to_bone(coordsys, bone_name)

Attaches COORDSYS (usually a world) to the bone named BONE_NAME.
As the bone moved (because of animation), COORDSYS will be moved too.
See tutorial lesson 015.
"""
    self.attached_coordsys.append((coordsys, self._bone_id(bone_name)))
    
  def detach_from_bone(self, coordsys):
    """Volume.detach_from_bone(coordsys)

Detaches COORDSYS from the bone it has been attached to."""
    for i in range(len(self.attached_coordsys)):
      if self.attached_coordsys[i][0] is coordsys:
        del self.attached_coordsys[i]
        return
      
