#!/usr/bin/env python
# $Id: eng.py,v 1.8 2006/11/11 04:53:28 devel Exp $
# @file
# @author   Jim E. Brooks  http://www.jimbrooks.org
# @brief    Python code built over Engine/World C++ interface.
#
# @verbatim
#
# Naming convention:
# s         self
# m*        Class member
# cptr      Pointer to a C/C++ object.
# mCm[]     member that is a const matrix
#
# @endverbatim
#
# @remarks  - A Position class is omitted intentionally.
#             Instead, a (float,float,float) tuple is used as a 3D position.
################################################################################

import sys, random

# Import modules from Palomino.
import ceng, ceng2, cworld

# Import constants from Palomino modules whose values originate from C++.
# This is why variables such as AXIS_X,Xx,etc seem to magically appear.
from ceng2 import *

################################################################################
# Misc.

XX = 0
YY = 1
ZZ = 2

def random_f( limit = 1.0 ):
    """ Random float in range {0,..,limit} """
    return random.random() * limit

def random_ui( end = 0x7fffffff ):
    """ Random unsigned integer in range {0,..,end-1} """
    return (random.randrange(end)) % end

def WORLD( coord ):
    """ World coords originating from constants must be passed to WORLD() in case world coord system is scaled. """
    return coord

################################################################################
# RGBA class.
#
# An RGBA object can be constructed from a tuple of floats or bytes,
# but when accessed (using subscripting), it always returns floats.

class RGBA(object):
    
    def __init__( s, rgba ):
        """ Construct RGBA from a tuple of 3 bytes or floats. """
        # s.mRgba, regardless of the input arg, will be stored as a tuple (float,float,float)
        # in range {0.0...1.0}.
        if isinstance( rgba, tuple ):
            if isinstance( rgba[0], float ):
                s.mRgba = rgba
            elif isinstance( rgba[0], int ):
                s.mRgba = (rgba[0]/256.0, rgba[1]/256.0, rgba[2]/256.0, rgba[3]/256.0)
        else:
            raise TypeError

    def __getitem__( s, i ):
        """ Return a RGBA component as a float in range {0.0...1.0}.  i must be {0,1,2,3}. """
        return s.mRgba[i]

################################################################################
# Matrix class based on a C++ const Matrix type.  The "C" mCm[] means const.
# Intentionally, this has no method to change matrix elements.

class ConstMatrix(object):
    
    def __init__( s, matrixCptr ):
        """ matrixCptr is a pointer to a C++ const float[]. """
        s.mCmCptr = matrixCptr   # pointer to const float[]

    def __getitem__( s, i ):
        """ Access a matrix element using the subscript operator []. """
        return ceng.GetMatrixElem( s.mCmCptr, i )

################################################################################
# Palomino 3D Object class based on its C++ counterpart.
#
# The C++ Object class is a base virtual class (can't be instantiated).
# Only Fixed or Dyna classes can be instantiated, which is why Object.__init__()
# is passed a cptr of an existing C++ Fixed or Dyna object.

class Object(object):

    def __init__( s, objCptr ):
        """ Construct a Python 3D object from an existing C++ 3D Object. """
        s.mCptr = objCptr

    def SetWorldVertexs( s, vertexListTup ):
        """ Set an Object's world vertex array from a Python list of tuples: [ (x,y,z), .. ] """
        ceng.ObjectSetWorldVertexs( s.mCptr, vertexListTup )

    def GetPolygonList( s ):
        """ Return existing polygon list. """
        return ceng.ObjectGetPolygonList( s.mCptr )

################################################################################
# Fixed (non-Dyna) 3D Object class based on its C++ counterpart.

class Fixed(Object):

    def __init__( s, quadrantCptr, position ):
        """ Construct new object.  position: (x,y,z) """
        # Passed Fixed's C pointer to the base Python class "Object".
        (x,y,z) = position
        Object.__init__( s, ceng.FixedNew(quadrantCptr,x,y,z) )

################################################################################
# Dyna class based on its C++ counterpart.

class Dyna(object):

    def __init__( s, dynaCptr ):
        """ Construct a Python Dyna from an existing C++ Dyna object. """
        s.mCptr = dynaCptr
        s.mCm = ConstMatrix( ceng.DynaGetConstMatrix( dynaCptr ) )

    def Rotate( s, axis, deg ):
        """ Rotate a Dyna. """
        ceng.DynaRotate( s.mCptr, axis, deg )

    def Translate( s, axis, inc ):
        """ Translate a Dyna. """
        ceng.DynaTranslate( s.mCptr, axis, inc )

    def GetPosition( s ):
        """ Return Dyna's position as a tuple (x,y,z). """
        return ceng.DynaGetPosition( s.mCptr )

    def SetOrigin( s, Oxyz, coord ):
        """ Set matrix origin coordinate of a Dyna.  Ox,Oy,Oz (not 0,1,2). """
        ceng.DynaSetOrigin( s.mCptr, Oxyz, coord )

    def SetThrottle( s, throttle ):
        """ Set Dyna's throttle (0..100). """
        ceng.DynaSetThrottle( s.mCptr, throttle )

    def SetAnimation( s, animation ):
        """ Set Dyna's animation mode (eAnimate enum). """
        ceng.DynaSetAnimation( s.mCptr, animation )

    def ChaseIncDistance( s, inc ):
        """ Change distance between Dyna and its chase-plane by default increment.
            inc = {-1,+1} (typically) """
        ceng.DynaChaseIncDistance( s.mCptr, inc )

    def PilotSelectTarget( s ):
        """ Tell Dyna's Pilot to select a target. """
        ceng.DynaPilotSelectTarget( s.mCptr )

################################################################################
# Polytex is for a textured polygon.
# It consists of the filename of a texture and a list of 2D texture coordinates.
# A pair of 2D texture coordinates correlated to a polygon's vertex.
# The same Polytex can be reused for multiple polygons.

class Polytex(object):

    def __init__( s, textureName, textureCoordList ):
        """ textureName     : string
            textureCoordList: [ (float,float), ... ] """
        s.mName   = textureName
        s.mCoords = textureCoordList

    def GetTextureName( s ):
        return s.mName

    def GetTextureTuple( s, i ):
        """ Return a tuple of (x,y) texture coordinates. """
        return s.mCoords[i]

    def GetTextureTupleCnt( s ):
        """ Return count of tuples of (x,y) texture coordinates (not individual coords).
            For example, returns 3 (not 6) for a textured triangle. """
        return len(s.mCoords)

################################################################################
# Polygon class based on its C++ counterpart.

class Polygon(object):

    def __init__( s, cptrPolygon ):
        s.mCptr = cptrPolygon

################################################################################
# PolygonList class based on its C++ counterpart.
# This wraps a Python object around an existing C++ PolygonList object.
#
# Building a PolygonList is slightly differ for convenience.
# Python AddPolygon() accepts a vix list (in C++, it's followed by
# a series of AddVix() calls).

class PolygonList(object):

    # Eg: polygonList = obj.PolygonList( obj.GetPolygonList() )
    def __init__( s, cptrPolygonList ):
        s.mCptr        = cptrPolygonList  # cptrPolygonList = GetPolygonList()
        s.mRgba        = None    # RGBA object (not a float tuple)
        s.mPolytexList = [ ]     # list of polygon texture data
        s.mPolytexIdx  = None    # None, not 0

    def EnableCull( s, flag ):
        ceng.PolygonListEnableCull( s.mCptr, flag )

    def SetRgba( s, rgba ):
        """ Set RGBA for the next polygon(s) to add.
            rgba must be an RGBA object (not a float tuple). """
        if isinstance( rgba, RGBA ):
            s.mRgba = rgba
        else:
            raise TypeError

    def SetPolytexList( s, polytexList ):
        """ Register a list of Polytex objects.  Referenced by SetPolytexIdx(). """
        if isinstance( polytexList[0], Polytex ):  # check first element
            s.mPolytexList = polytexList
        else:
            raise TypeError

    def SetPolytexIdx( s, polytexIdx ):
        """ Specify a texture for the next polygon(s) to add.
            The texture info is referenced by the passed index within mPolytexList[]. """
        s.mPolytexIdx = polytexIdx

    def NoTexture( s ):
        """ Specify no texturing for the next polygon(s) to add. """
        s.mPolytexIdx = None   # None, not 0

    def IfTexture( s ):
        """ Return true if the current polygon (being added) has a texture. """
        return s.mPolytexIdx >= 0  # if not None

    def GetTextureName( s ):
        """ Return texture name of current polygon (being added) else None. """
        try:
            return s.mPolytexList[s.mPolytexIdx].GetTextureName()
        except:
            return None

    def GetTextureTupleCnt( s ):
        """ Return current count of the tuples of 2D texture coordinates (0 if no texture). """
        try:
            return s.mPolytexList[s.mPolytexIdx].GetTextureTupleCnt()
        except:
            return 0

    def GetTextureTuple( s, i ):
        """ Return current texture coord tuple (x,y). """
        try:
            return s.mPolytexList[s.mPolytexIdx].GetTextureTuple( i )
        except:
            return None

    def AddPolygon( s, vixList ):
        """ Both creates and adds a polygon to the list using the currently set RGBA/texture data.
            Returns a Polygon object. """

        # Build PolygonAttribs.
        if s.IfTexture():
            # New PolygonAttribs with texture.
            attribsCptr = ceng.PolygonAttribsNew2( s.mRgba[0], s.mRgba[1], s.mRgba[2], s.mRgba[3], s.GetTextureName(), s.GetTextureTupleCnt() )
            # Add texture coord tuples (x,y).
            textureTupleCnt = s.GetTextureTupleCnt()
            if not textureTupleCnt:
                raise IndexError
            i = 0
            while i < textureTupleCnt:
                (x,y) = s.GetTextureTuple(i)
                ceng.PolygonAttribsAddTextureTup( attribsCptr, x, y )
                i += 1
        else:
            attribsCptr = ceng.PolygonAttribsNew( s.mRgba[0], s.mRgba[1], s.mRgba[2], s.mRgba[3] )

        # Both construct a Polygon and add it to the PolygonList.
        polygonCptr = ceng.PolygonListAddPolygon( s.mCptr, attribsCptr, len(vixList) )

        # Add each vix.
        for vix in vixList:
            ceng.PolygonAddVix( polygonCptr, vix )

        # Delete PolygonAttribs (a shared copy of it was made).
        ceng.PolygonAttribsDelete( attribsCptr )

        # Done.  Don't delete the Polygon as it will be returned.
        # Even if it wasn't returned, PolygonList is supposed to delete it.
        return Polygon( polygonCptr )
        
    def Optimize( s, vertexCnt ):
        """ This must be called when the polygon list is complete. """
        ceng.PolygonListOptimize( s.mCptr, vertexCnt )
