/*
===========================================================================
Copyright (C) 2000-2013 Darklegion Development

This file is part of Tremulous.

Tremulous 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.

Tremulous 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 Tremulous; if not, write to the Free Software
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
===========================================================================
*/

// cg_trails.c -- the trail system


#include "cg_local.h"

static baseTrailSystem_t  baseTrailSystems[ MAX_BASETRAIL_SYSTEMS ];
static baseTrailBeam_t    baseTrailBeams[ MAX_BASETRAIL_BEAMS ];
static int                numBaseTrailSystems = 0;
static int                numBaseTrailBeams = 0;

static trailSystem_t      trailSystems[ MAX_TRAIL_SYSTEMS ];
static trailBeam_t        trailBeams[ MAX_TRAIL_BEAMS ];

/*
===============
CG_CalculateBeamNodeProperties

Fills in trailBeamNode_t.textureCoord
===============
*/
static void CG_CalculateBeamNodeProperties( trailBeam_t *tb )
{
  trailBeamNode_t *i = NULL;
  trailSystem_t   *ts;
  baseTrailBeam_t *btb;
  float           nodeDistances[ MAX_TRAIL_BEAM_NODES ];
  float           totalDistance = 0.0f, position = 0.0f;
  int             j, numNodes = 0;
  float           TCRange, widthRange, alphaRange;
  vec3_t          colorRange;
  float           fadeAlpha = 1.0f;

  if( !tb || !tb->nodes )
    return;

  ts = tb->parent;
  btb = tb->class;

  if( ts->destroyTime > 0 && btb->fadeOutTime )
  {
    fadeAlpha -= (float)( cg.time - ts->destroyTime ) / btb->fadeOutTime;

    if( fadeAlpha < 0.0f )
      fadeAlpha = 0.0f;
  }

  TCRange = tb->class->backTextureCoord -
    tb->class->frontTextureCoord;
  widthRange = tb->class->backWidth -
    tb->class->frontWidth;
  alphaRange = tb->class->backAlpha -
    tb->class->frontAlpha;
  VectorSubtract( tb->class->backColor,
      tb->class->frontColor, colorRange );

  for( i = tb->nodes; i && i->next; i = i->next )
  {
    nodeDistances[ numNodes++ ] =
      Distance( i->position, i->next->position );
  }

  for( j = 0; j < numNodes; j++ )
    totalDistance += nodeDistances[ j ];

  for( j = 0, i = tb->nodes; i; i = i->next, j++ )
  {
    if( tb->class->textureType == TBTT_STRETCH )
    {
      i->textureCoord = tb->class->frontTextureCoord +
        ( ( position / totalDistance ) * TCRange );
    }
    else if( tb->class->textureType == TBTT_REPEAT )
    {
      if( tb->class->clampToBack )
        i->textureCoord = ( totalDistance - position ) /
          tb->class->repeatLength;
      else
        i->textureCoord = position / tb->class->repeatLength;
    }

    i->halfWidth = ( tb->class->frontWidth +
      ( ( position / totalDistance ) * widthRange ) ) / 2.0f;
    i->alpha = (byte)( (float)0xFF * ( tb->class->frontAlpha +
      ( ( position / totalDistance ) * alphaRange ) ) * fadeAlpha );
    VectorMA( tb->class->frontColor, ( position / totalDistance ),
        colorRange, i->color );

    position += nodeDistances[ j ];
  }
}

/*
===============
CG_LightVertex

Lights a particular vertex
===============
*/
static void CG_LightVertex( vec3_t point, byte alpha, byte *rgba )
{
  int     i;
  vec3_t  alight, dlight, lightdir;

  trap_R_LightForPoint( point, alight, dlight, lightdir );
  for( i = 0; i <= 2; i++ )
    rgba[ i ] = (int)alight[ i ];

  rgba[ 3 ] = alpha;
}

/*
===============
CG_RenderBeam

Renders a beam
===============
*/
static void CG_RenderBeam( trailBeam_t *tb )
{
  trailBeamNode_t   *i = NULL;
  trailBeamNode_t   *prev = NULL;
  trailBeamNode_t   *next = NULL;
  vec3_t            up;
  polyVert_t        verts[ ( MAX_TRAIL_BEAM_NODES - 1 ) * 4 ];
  int               numVerts = 0;
  baseTrailBeam_t   *btb;
  trailSystem_t     *ts;
  baseTrailSystem_t *bts;

  if( !tb || !tb->nodes )
    return;

  btb = tb->class;
  ts = tb->parent;
  bts = ts->class;

  if( bts->thirdPersonOnly &&
      ( CG_AttachmentCentNum( &ts->frontAttachment ) == cg.snap->ps.clientNum ||
        CG_AttachmentCentNum( &ts->backAttachment ) == cg.snap->ps.clientNum ) &&
      !cg.renderingThirdPerson )
    return;

  CG_CalculateBeamNodeProperties( tb );

  i = tb->nodes;

  do
  {
    prev = i->prev;
    next = i->next;

    if( prev && next )
    {
      //this node has two neighbours
      GetPerpendicularViewVector( cg.refdef.vieworg, next->position, prev->position, up );
    }
    else if( !prev && next )
    {
      //this is the front
      GetPerpendicularViewVector( cg.refdef.vieworg, next->position, i->position, up );
    }
    else if( prev && !next )
    {
      //this is the back
      GetPerpendicularViewVector( cg.refdef.vieworg, i->position, prev->position, up );
    }
    else
      break;

    if( prev )
    {
      VectorMA( i->position, i->halfWidth, up, verts[ numVerts ].xyz );
      verts[ numVerts ].st[ 0 ] = i->textureCoord;
      verts[ numVerts ].st[ 1 ] = 1.0f;

      if( btb->realLight )
        CG_LightVertex( verts[ numVerts ].xyz, i->alpha, verts[ numVerts ].modulate );
      else
      {
        VectorCopy( i->color, verts[ numVerts ].modulate );
        verts[ numVerts ].modulate[ 3 ] = i->alpha;
      }

      numVerts++;

      VectorMA( i->position, -i->halfWidth, up, verts[ numVerts ].xyz );
      verts[ numVerts ].st[ 0 ] = i->textureCoord;
      verts[ numVerts ].st[ 1 ] = 0.0f;

      if( btb->realLight )
        CG_LightVertex( verts[ numVerts ].xyz, i->alpha, verts[ numVerts ].modulate );
      else
      {
        VectorCopy( i->color, verts[ numVerts ].modulate );
        verts[ numVerts ].modulate[ 3 ] = i->alpha;
      }

      numVerts++;
    }

    if( next )
    {
      VectorMA( i->position, -i->halfWidth, up, verts[ numVerts ].xyz );
      verts[ numVerts ].st[ 0 ] = i->textureCoord;
      verts[ numVerts ].st[ 1 ] = 0.0f;

      if( btb->realLight )
        CG_LightVertex( verts[ numVerts ].xyz, i->alpha, verts[ numVerts ].modulate );
      else
      {
        VectorCopy( i->color, verts[ numVerts ].modulate );
        verts[ numVerts ].modulate[ 3 ] = i->alpha;
      }

      numVerts++;

      VectorMA( i->position, i->halfWidth, up, verts[ numVerts ].xyz );
      verts[ numVerts ].st[ 0 ] = i->textureCoord;
      verts[ numVerts ].st[ 1 ] = 1.0f;

      if( btb->realLight )
        CG_LightVertex( verts[ numVerts ].xyz, i->alpha, verts[ numVerts ].modulate );
      else
      {
        VectorCopy( i->color, verts[ numVerts ].modulate );
        verts[ numVerts ].modulate[ 3 ] = i->alpha;
      }

      numVerts++;
    }

    i = i->next;
  } while( i );

  trap_R_AddPolysToScene( tb->class->shader, 4, &verts[ 0 ], numVerts / 4 );
}

/*
===============
CG_AllocateBeamNode

Allocates a trailBeamNode_t from a trailBeam_t's nodePool
===============
*/
static trailBeamNode_t *CG_AllocateBeamNode( trailBeam_t *tb )
{
  baseTrailBeam_t *btb = tb->class;
  int             i;
  trailBeamNode_t *tbn;

  for( i = 0; i < MAX_TRAIL_BEAM_NODES; i++ )
  {
    tbn = &tb->nodePool[ i ];
    if( !tbn->used )
    {
      tbn->timeLeft = btb->segmentTime;
      tbn->prev = NULL;
      tbn->next = NULL;
      tbn->used = qtrue;
      return tbn;
    }
  }

  // no space left
  return NULL;
}

/*
===============
CG_DestroyBeamNode

Removes a node from a beam
Returns the new head
===============
*/
static trailBeamNode_t *CG_DestroyBeamNode( trailBeamNode_t *tbn )
{
  trailBeamNode_t *newHead = NULL;

  if( tbn->prev )
  {
    if( tbn->next )
    {
      // node is in the middle
      tbn->prev->next = tbn->next;
      tbn->next->prev = tbn->prev;
    }
    else // node is at the back
      tbn->prev->next = NULL;

    // find the new head (shouldn't have changed)
    newHead = tbn->prev;

    while( newHead->prev )
      newHead = newHead->prev;
  }
  else if( tbn->next )
  {
    //node is at the front
    tbn->next->prev = NULL;
    newHead = tbn->next;
  }

  tbn->prev = NULL;
  tbn->next = NULL;
  tbn->used = qfalse;

  return newHead;
}

/*
===============
CG_FindLastBeamNode

Returns the last beam node in a beam
===============
*/
static trailBeamNode_t *CG_FindLastBeamNode( trailBeam_t *tb )
{
  trailBeamNode_t *i = tb->nodes;

  while( i && i->next )
    i = i->next;

  return i;
}

/*
===============
CG_CountBeamNodes

Returns the number of nodes in a beam
===============
*/
static int CG_CountBeamNodes( trailBeam_t *tb )
{
  trailBeamNode_t *i = tb->nodes;
  int             numNodes = 0;

  while( i )
  {
    numNodes++;
    i = i->next;
  }

  return numNodes;
}

/*
===============
CG_PrependBeamNode

Prepend a new beam node to the front of a beam
Returns the new node
===============
*/
static trailBeamNode_t *CG_PrependBeamNode( trailBeam_t *tb )
{
  trailBeamNode_t *i;

  if( tb->nodes )
  {
    // prepend another node
    i = CG_AllocateBeamNode( tb );

    if( i )
    {
      i->next = tb->nodes;
      tb->nodes->prev = i;
      tb->nodes = i;
    }
  }
  else //add first node
  {
    i = CG_AllocateBeamNode( tb );

    if( i )
      tb->nodes = i;
  }

  return i;
}

/*
===============
CG_AppendBeamNode

Append a new beam node to the back of a beam
Returns the new node
===============
*/
static trailBeamNode_t *CG_AppendBeamNode( trailBeam_t *tb )
{
  trailBeamNode_t *last, *i;

  if( tb->nodes )
  {
    // append another node
    last = CG_FindLastBeamNode( tb );
    i = CG_AllocateBeamNode( tb );

    if( i )
    {
      last->next = i;
      i->prev = last;
      i->next = NULL;
    }
  }
  else //add first node
  {
    i = CG_AllocateBeamNode( tb );

    if( i )
      tb->nodes = i;
  }

  return i;
}

/*
===============
CG_ApplyJitters
===============
*/
static void CG_ApplyJitters( trailBeam_t *tb )
{
  trailBeamNode_t *i = NULL;
  int             j;
  baseTrailBeam_t *btb;
  trailSystem_t   *ts;
  trailBeamNode_t *start;
  trailBeamNode_t *end;

  if( !tb || !tb->nodes )
    return;

  btb = tb->class;
  ts = tb->parent;

  for( j = 0; j < btb->numJitters; j++ )
  {
    if( tb->nextJitterTimes[ j ] <= cg.time )
    {
      for( i = tb->nodes; i; i = i->next )
      {
        i->jitters[ j ][ 0 ] = ( crandom( ) * btb->jitters[ j ].magnitude );
        i->jitters[ j ][ 1 ] = ( crandom( ) * btb->jitters[ j ].magnitude );
      }

      tb->nextJitterTimes[ j ] = cg.time + btb->jitters[ j ].period;
    }
  }

  start = tb->nodes;
  end = CG_FindLastBeamNode( tb );

  if( !btb->jitterAttachments )
  {
    if( CG_Attached( &ts->frontAttachment ) && start->next )
      start = start->next;

    if( CG_Attached( &ts->backAttachment ) && end->prev )
      end = end->prev;
  }

  for( i = start; i; i = i->next )
  {
    vec3_t          forward, right, up;
    trailBeamNode_t *prev;
    trailBeamNode_t *next;
    float           upJitter = 0.0f, rightJitter = 0.0f;

    prev = i->prev;
    next = i->next;

    if( prev && next )
    {
      //this node has two neighbours
      GetPerpendicularViewVector( cg.refdef.vieworg, next->position, prev->position, up );
      VectorSubtract( next->position, prev->position, forward );
    }
    else if( !prev && next )
    {
      //this is the front
      GetPerpendicularViewVector( cg.refdef.vieworg, next->position, i->position, up );
      VectorSubtract( next->position, i->position, forward );
    }
    else if( prev && !next )
    {
      //this is the back
      GetPerpendicularViewVector( cg.refdef.vieworg, i->position, prev->position, up );
      VectorSubtract( i->position, prev->position, forward );
    }

    VectorNormalize( forward );
    CrossProduct( forward, up, right );
    VectorNormalize( right );

    for( j = 0; j < btb->numJitters; j++ )
    {
      upJitter += i->jitters[ j ][ 0 ];
      rightJitter += i->jitters[ j ][ 1 ];
    }

    VectorMA( i->position, upJitter, up, i->position );
    VectorMA( i->position, rightJitter, right, i->position );

    if( i == end )
      break;
  }
}

/*
===============
CG_UpdateBeam

Updates a beam
===============
*/
static void CG_UpdateBeam( trailBeam_t *tb )
{
  baseTrailBeam_t *btb;
  trailSystem_t   *ts;
  trailBeamNode_t *i;
  int             deltaTime;
  int             nodesToAdd;
  int             j;
  int             numNodes;

  if( !tb )
    return;

  btb = tb->class;
  ts = tb->parent;

  deltaTime = cg.time - tb->lastEvalTime;
  tb->lastEvalTime = cg.time;

  // first make sure this beam has enough nodes
  if( ts->destroyTime <= 0 || btb->fadeOutTime > 0 )
  {
    nodesToAdd = btb->numSegments - CG_CountBeamNodes( tb ) + 1;

    while( nodesToAdd-- )
    {
      i = CG_AppendBeamNode( tb );

      if( !tb->nodes->next && CG_Attached( &ts->frontAttachment ) )
      {
        // this is the first node to be added
        if( !CG_AttachmentPoint( &ts->frontAttachment, i->refPosition ) )
          CG_DestroyTrailSystem( &ts );
      }
      else
        VectorCopy( i->prev->refPosition, i->refPosition );
    }
  }

  numNodes = CG_CountBeamNodes( tb );

  for( i = tb->nodes; i; i = i->next )
    VectorCopy( i->refPosition, i->position );

  if( CG_Attached( &ts->frontAttachment ) && CG_Attached( &ts->backAttachment ) )
  {
    // beam between two attachments
    vec3_t dir, front, back;

    if( ts->destroyTime > 0 && ( cg.time - ts->destroyTime ) >= btb->fadeOutTime )
    {
      tb->valid = qfalse;
      return;
    }

    if( !CG_AttachmentPoint( &ts->frontAttachment, front ) )
      CG_DestroyTrailSystem( &ts );

    if( !CG_AttachmentPoint( &ts->backAttachment, back ) )
      CG_DestroyTrailSystem( &ts );

    VectorSubtract( back, front, dir );

    for( j = 0, i = tb->nodes; i; i = i->next, j++ )
    {
      float scale = (float)j / (float)( numNodes - 1 );

      VectorMA( front, scale, dir, i->position );
    }
  }
  else if( CG_Attached( &ts->frontAttachment ) )
  {
    // beam from one attachment

    // cull the trail tail
    i = CG_FindLastBeamNode( tb );

    if( i && i->timeLeft >= 0 )
    {
      i->timeLeft -= deltaTime;

      if( i->timeLeft < 0 )
      {
        tb->nodes = CG_DestroyBeamNode( i );

        if( !tb->nodes )
        {
          tb->valid = qfalse;
          return;
        }

        // if the ts has been destroyed, stop creating new nodes
        if( ts->destroyTime <= 0 )
          CG_PrependBeamNode( tb );
      }
      else if( i->timeLeft >= 0 && i->prev )
      {
        vec3_t  dir;
        float   length;

        VectorSubtract( i->refPosition, i->prev->refPosition, dir );
        length = VectorNormalize( dir ) *
          ( (float)i->timeLeft / (float)tb->class->segmentTime );

        VectorMA( i->prev->refPosition, length, dir, i->position );
      }
    }

    if( tb->nodes )
    {
      if( !CG_AttachmentPoint( &ts->frontAttachment, tb->nodes->refPosition ) )
        CG_DestroyTrailSystem( &ts );

      VectorCopy( tb->nodes->refPosition, tb->nodes->position );
    }
  }

  CG_ApplyJitters( tb );
}

/*
===============
CG_ParseTrailBeamColor
===============
*/
static qboolean CG_ParseTrailBeamColor( byte *c, char **text_p )
{
  char  *token;
  int   i;

  for( i = 0; i <= 2; i++ )
  {
    token = COM_Parse( text_p );

    if( !Q_stricmp( token, "" ) )
      return qfalse;

    c[ i ] = (int)( (float)0xFF * atof_neg( token, qfalse ) );
  }

  return qtrue;
}

/*
===============
CG_ParseTrailBeam

Parse a trail beam
===============
*/
static qboolean CG_ParseTrailBeam( baseTrailBeam_t *btb, char **text_p )
{
  char  *token;

  // read optional parameters
  while( 1 )
  {
    token = COM_Parse( text_p );

    if( !Q_stricmp( token, "" ) )
      return qfalse;

    if( !Q_stricmp( token, "segments" ) )
    {
      token = COM_Parse( text_p );
      if( !Q_stricmp( token, "" ) )
        break;

      btb->numSegments = atoi_neg( token, qfalse );

      if( btb->numSegments >= MAX_TRAIL_BEAM_NODES )
      {
        btb->numSegments = MAX_TRAIL_BEAM_NODES - 1;
        CG_Printf( S_COLOR_YELLOW "WARNING: too many segments in trail beam\n" );
      }
      continue;
    }
    else if( !Q_stricmp( token, "width" ) )
    {
      token = COM_Parse( text_p );
      if( !Q_stricmp( token, "" ) )
        break;

      btb->frontWidth = atof_neg( token, qfalse );

      token = COM_Parse( text_p );
      if( !Q_stricmp( token, "" ) )
        break;

      if( !Q_stricmp( token, "-" ) )
        btb->backWidth = btb->frontWidth;
      else
        btb->backWidth = atof_neg( token, qfalse );
      continue;
    }
    else if( !Q_stricmp( token, "alpha" ) )
    {
      token = COM_Parse( text_p );
      if( !Q_stricmp( token, "" ) )
        break;

      btb->frontAlpha = atof_neg( token, qfalse );

      token = COM_Parse( text_p );
      if( !Q_stricmp( token, "" ) )
        break;

      if( !Q_stricmp( token, "-" ) )
        btb->backAlpha = btb->frontAlpha;
      else
        btb->backAlpha = atof_neg( token, qfalse );
      continue;
    }
    else if( !Q_stricmp( token, "color" ) )
    {
      token = COM_Parse( text_p );
      if( !Q_stricmp( token, "" ) )
        break;

      if( !Q_stricmp( token, "{" ) )
      {
        if( !CG_ParseTrailBeamColor( btb->frontColor, text_p ) )
          break;

        token = COM_Parse( text_p );
        if( Q_stricmp( token, "}" ) )
        {
          CG_Printf( S_COLOR_RED "ERROR: missing '}'\n" );
          break;
        }

        token = COM_Parse( text_p );
        if( !Q_stricmp( token, "" ) )
          break;

        if( !Q_stricmp( token, "-" ) )
        {
          btb->backColor[ 0 ] = btb->frontColor[ 0 ];
          btb->backColor[ 1 ] = btb->frontColor[ 1 ];
          btb->backColor[ 2 ] = btb->frontColor[ 2 ];
        }
        else if( !Q_stricmp( token, "{" ) )
        {
          if( !CG_ParseTrailBeamColor( btb->backColor, text_p ) )
            break;

          token = COM_Parse( text_p );
          if( Q_stricmp( token, "}" ) )
          {
            CG_Printf( S_COLOR_RED "ERROR: missing '}'\n" );
            break;
          }
        }
        else
        {
          CG_Printf( S_COLOR_RED "ERROR: missing '{'\n" );
          break;
        }
      }
      else
      {
        CG_Printf( S_COLOR_RED "ERROR: missing '{'\n" );
        break;
      }

      continue;
    }
    else if( !Q_stricmp( token, "segmentTime" ) )
    {
      token = COM_Parse( text_p );
      if( !Q_stricmp( token, "" ) )
        break;

      btb->segmentTime = atoi_neg( token, qfalse );
      continue;
    }
    else if( !Q_stricmp( token, "fadeOutTime" ) )
    {
      token = COM_Parse( text_p );
      if( !Q_stricmp( token, "" ) )
        break;

      btb->fadeOutTime = atoi_neg( token, qfalse );
      continue;
    }
    else if( !Q_stricmp( token, "shader" ) )
    {
      token = COM_Parse( text_p );
      if( !Q_stricmp( token, "" ) )
        break;

      Q_strncpyz( btb->shaderName, token, MAX_QPATH );

      continue;
    }
    else if( !Q_stricmp( token, "textureType" ) )
    {
      token = COM_Parse( text_p );
      if( !Q_stricmp( token, "" ) )
        break;

      if( !Q_stricmp( token, "stretch" ) )
      {
        btb->textureType = TBTT_STRETCH;

        token = COM_Parse( text_p );
        if( !Q_stricmp( token, "" ) )
          break;

        btb->frontTextureCoord = atof_neg( token, qfalse );

        token = COM_Parse( text_p );
        if( !Q_stricmp( token, "" ) )
          break;

        btb->backTextureCoord = atof_neg( token, qfalse );
      }
      else if( !Q_stricmp( token, "repeat" ) )
      {
        btb->textureType = TBTT_REPEAT;

        token = COM_Parse( text_p );
        if( !Q_stricmp( token, "" ) )
          break;

        if( !Q_stricmp( token, "front" ) )
          btb->clampToBack = qfalse;
        else if( !Q_stricmp( token, "back" ) )
          btb->clampToBack = qtrue;
        else
        {
          CG_Printf( S_COLOR_RED "ERROR: unknown textureType clamp \"%s\"\n", token );
          break;
        }

        token = COM_Parse( text_p );
        if( !Q_stricmp( token, "" ) )
          break;

        btb->repeatLength = atof_neg( token, qfalse );
      }
      else
      {
        CG_Printf( S_COLOR_RED "ERROR: unknown textureType \"%s\"\n", token );
        break;
      }

      continue;
    }
    else if( !Q_stricmp( token, "realLight" ) )
    {
      btb->realLight = qtrue;

      continue;
    }
    else if( !Q_stricmp( token, "jitter" ) )
    {
      if( btb->numJitters == MAX_TRAIL_BEAM_JITTERS )
      {
        CG_Printf( S_COLOR_RED "ERROR: too many jitters\n" );
        break;
      }

      token = COM_Parse( text_p );
      if( !Q_stricmp( token, "" ) )
        break;

      btb->jitters[ btb->numJitters ].magnitude = atof_neg( token, qfalse );

      token = COM_Parse( text_p );
      if( !Q_stricmp( token, "" ) )
        break;

      btb->jitters[ btb->numJitters ].period = atoi_neg( token, qfalse );

      btb->numJitters++;

      continue;
    }
    else if( !Q_stricmp( token, "jitterAttachments" ) )
    {
      btb->jitterAttachments = qtrue;

      continue;
    }
    else if( !Q_stricmp( token, "}" ) )
      return qtrue; //reached the end of this trail beam
    else
    {
      CG_Printf( S_COLOR_RED "ERROR: unknown token '%s' in trail beam\n", token );
      return qfalse;
    }
  }

  return qfalse;
}

/*
===============
CG_InitialiseBaseTrailBeam
===============
*/
static void CG_InitialiseBaseTrailBeam( baseTrailBeam_t *btb )
{
  memset( btb, 0, sizeof( baseTrailBeam_t ) );

  btb->numSegments = 1;
  btb->frontWidth = btb->backWidth = 1.0f;
  btb->frontAlpha = btb->backAlpha = 1.0f;
  memset( btb->frontColor, 0xFF, sizeof( btb->frontColor ) );
  memset( btb->backColor, 0xFF, sizeof( btb->backColor ) );

  btb->segmentTime = 100;

  btb->textureType = TBTT_STRETCH;
  btb->frontTextureCoord = 0.0f;
  btb->backTextureCoord = 1.0f;
}

/*
===============
CG_ParseTrailSystem

Parse a trail system section
===============
*/
static qboolean CG_ParseTrailSystem( baseTrailSystem_t *bts, char **text_p, const char *name )
{
  char *token;

  // read optional parameters
  while( 1 )
  {
    token = COM_Parse( text_p );

    if( !Q_stricmp( token, "" ) )
      return qfalse;

    if( !Q_stricmp( token, "{" ) )
    {
      CG_InitialiseBaseTrailBeam( &baseTrailBeams[ numBaseTrailBeams ] );

      if( !CG_ParseTrailBeam( &baseTrailBeams[ numBaseTrailBeams ], text_p ) )
      {
        CG_Printf( S_COLOR_RED "ERROR: failed to parse trail beam\n" );
        return qfalse;
      }

      if( bts->numBeams == MAX_BEAMS_PER_SYSTEM )
      {
        CG_Printf( S_COLOR_RED "ERROR: trail system has > %d beams\n", MAX_BEAMS_PER_SYSTEM );
        return qfalse;
      }
      else if( numBaseTrailBeams == MAX_BASETRAIL_BEAMS )
      {
        CG_Printf( S_COLOR_RED "ERROR: maximum number of trail beams (%d) reached\n",
            MAX_BASETRAIL_BEAMS );
        return qfalse;
      }
      else
      {
        //start parsing beams again
        bts->beams[ bts->numBeams ] = &baseTrailBeams[ numBaseTrailBeams ];
        bts->numBeams++;
        numBaseTrailBeams++;
      }
      continue;
    }
    else if( !Q_stricmp( token, "thirdPersonOnly" ) )
      bts->thirdPersonOnly = qtrue;
    else if( !Q_stricmp( token, "lifeTime" ) )
    {
      token = COM_Parse( text_p );
      if( !Q_stricmp( token, "" ) )
        break;

      bts->lifeTime = atoi_neg( token, qfalse );
      continue;
    }
    else if( !Q_stricmp( token, "beam" ) ) //acceptable text
      continue;
    else if( !Q_stricmp( token, "}" ) )
    {
      if( cg_debugTrails.integer >= 1 )
        CG_Printf( "Parsed trail system %s\n", name );

      return qtrue; //reached the end of this trail system
    }
    else
    {
      CG_Printf( S_COLOR_RED "ERROR: unknown token '%s' in trail system %s\n", token, bts->name );
      return qfalse;
    }
  }

  return qfalse;
}

/*
===============
CG_ParseTrailFile

Load the trail systems from a trail file
===============
*/
static qboolean CG_ParseTrailFile( const char *fileName )
{
  char          *text_p;
  int           i;
  int           len;
  char          *token;
  char          text[ 32000 ];
  char          tsName[ MAX_QPATH ];
  qboolean      tsNameSet = qfalse;
  fileHandle_t  f;

  // load the file
  len = trap_FS_FOpenFile( fileName, &f, FS_READ );
  if( len <= 0 )
    return qfalse;

  if( len == 0 || len >= sizeof( text ) - 1 )
  {
    trap_FS_FCloseFile( f );
    CG_Printf( S_COLOR_RED "ERROR: trail file %s is %s\n", fileName,
      len == 0 ? "empty" : "too long" );
    return qfalse;
  }

  trap_FS_Read( text, len, f );
  text[ len ] = 0;
  trap_FS_FCloseFile( f );

  // parse the text
  text_p = text;

  // read optional parameters
  while( 1 )
  {
    token = COM_Parse( &text_p );

    if( !Q_stricmp( token, "" ) )
      break;

    if( !Q_stricmp( token, "{" ) )
    {
      if( tsNameSet )
      {
        //check for name space clashes
        for( i = 0; i < numBaseTrailSystems; i++ )
        {
          if( !Q_stricmp( baseTrailSystems[ i ].name, tsName ) )
          {
            CG_Printf( S_COLOR_RED "ERROR: a trail system is already named %s\n", tsName );
            return qfalse;
          }
        }

        Q_strncpyz( baseTrailSystems[ numBaseTrailSystems ].name, tsName, MAX_QPATH );

        if( !CG_ParseTrailSystem( &baseTrailSystems[ numBaseTrailSystems ], &text_p, tsName ) )
        {
          CG_Printf( S_COLOR_RED "ERROR: %s: failed to parse trail system %s\n", fileName, tsName );
          return qfalse;
        }

        //start parsing trail systems again
        tsNameSet = qfalse;

        if( numBaseTrailSystems == MAX_BASETRAIL_SYSTEMS )
        {
          CG_Printf( S_COLOR_RED "ERROR: maximum number of trail systems (%d) reached\n",
              MAX_BASETRAIL_SYSTEMS );
          return qfalse;
        }
        else
          numBaseTrailSystems++;

        continue;
      }
      else
      {
        CG_Printf( S_COLOR_RED "ERROR: unamed trail system\n" );
        return qfalse;
      }
    }

    if( !tsNameSet )
    {
      Q_strncpyz( tsName, token, sizeof( tsName ) );
      tsNameSet = qtrue;
    }
    else
    {
      CG_Printf( S_COLOR_RED "ERROR: trail system already named\n" );
      return qfalse;
    }
  }

  return qtrue;
}

/*
===============
CG_LoadTrailSystems

Load trail system templates
===============
*/
void CG_LoadTrailSystems( void )
{
  int   i, numFiles, fileLen;
  char  fileList[ MAX_TRAIL_FILES * MAX_QPATH ];
  char  fileName[ MAX_QPATH ];
  char  *filePtr;

  //clear out the old
  numBaseTrailSystems = 0;
  numBaseTrailBeams = 0;

  for( i = 0; i < MAX_BASETRAIL_SYSTEMS; i++ )
  {
    baseTrailSystem_t  *bts = &baseTrailSystems[ i ];
    memset( bts, 0, sizeof( baseTrailSystem_t ) );
  }

  for( i = 0; i < MAX_BASETRAIL_BEAMS; i++ )
  {
    baseTrailBeam_t  *btb = &baseTrailBeams[ i ];
    memset( btb, 0, sizeof( baseTrailBeam_t ) );
  }

  //and bring in the new
  numFiles = trap_FS_GetFileList( "scripts", ".trail",
      fileList, MAX_TRAIL_FILES * MAX_QPATH );
  filePtr = fileList;

  for( i = 0; i < numFiles; i++, filePtr += fileLen + 1 )
  {
    fileLen = strlen( filePtr );
    strcpy( fileName, "scripts/" );
    strcat( fileName, filePtr );
    CG_Printf( "...loading '%s'\n", fileName );
    CG_ParseTrailFile( fileName );
  }
}

/*
===============
CG_RegisterTrailSystem

Load the media that a trail system needs
===============
*/
qhandle_t CG_RegisterTrailSystem( char *name )
{
  int               i, j;
  baseTrailSystem_t *bts;
  baseTrailBeam_t   *btb;

  for( i = 0; i < MAX_BASETRAIL_SYSTEMS; i++ )
  {
    bts = &baseTrailSystems[ i ];

    if( !Q_stricmp( bts->name, name ) )
    {
      //already registered
      if( bts->registered )
        return i + 1;

      for( j = 0; j < bts->numBeams; j++ )
      {
        btb = bts->beams[ j ];

        btb->shader = trap_R_RegisterShader( btb->shaderName );
      }

      if( cg_debugTrails.integer >= 1 )
        CG_Printf( "Registered trail system %s\n", name );

      bts->registered = qtrue;

      //avoid returning 0
      return i + 1;
    }
  }

  CG_Printf( S_COLOR_RED "ERROR: failed to register trail system %s\n", name );
  return 0;
}


/*
===============
CG_SpawnNewTrailBeam

Allocate a new trail beam
===============
*/
static trailBeam_t *CG_SpawnNewTrailBeam( baseTrailBeam_t *btb,
    trailSystem_t *parent )
{
  int           i;
  trailBeam_t   *tb = NULL;
  trailSystem_t *ts = parent;

  for( i = 0; i < MAX_TRAIL_BEAMS; i++ )
  {
    tb = &trailBeams[ i ];

    if( !tb->valid )
    {
      memset( tb, 0, sizeof( trailBeam_t ) );

      //found a free slot
      tb->class = btb;
      tb->parent = ts;

      tb->valid = qtrue;

      if( cg_debugTrails.integer >= 1 )
        CG_Printf( "TB %s created\n", ts->class->name );

      return tb;
    }
  }

  if( cg_debugTrails.integer >= 1 )
    CG_Printf( "MAX_TRAIL_BEAMS\n" );

  return NULL;
}


/*
===============
CG_SpawnNewTrailSystem

Spawns a new trail system
===============
*/
trailSystem_t *CG_SpawnNewTrailSystem( qhandle_t psHandle )
{
  int               i, j;
  trailSystem_t     *ts = NULL;
  baseTrailSystem_t *bts = &baseTrailSystems[ psHandle - 1 ];

  if( !bts->registered )
  {
    CG_Printf( S_COLOR_RED "ERROR: a trail system has not been registered yet\n" );
    return NULL;
  }

  for( i = 0; i < MAX_TRAIL_SYSTEMS; i++ )
  {
    ts = &trailSystems[ i ];

    if( !ts->valid )
    {
      memset( ts, 0, sizeof( trailSystem_t ) );

      //found a free slot
      ts->class = bts;

      ts->valid = qtrue;
      ts->destroyTime = -1;
      ts->birthTime = cg.time;
      
      for( j = 0; j < bts->numBeams; j++ )
        CG_SpawnNewTrailBeam( bts->beams[ j ], ts );

      if( cg_debugTrails.integer >= 1 )
        CG_Printf( "TS %s created\n", bts->name );

      return ts;
    }
  }

  if( cg_debugTrails.integer >= 1 )
    CG_Printf( "MAX_TRAIL_SYSTEMS\n" );

  return NULL;
}

/*
===============
CG_DestroyTrailSystem

Destroy a trail system
===============
*/
void CG_DestroyTrailSystem( trailSystem_t **ts )
{
  (*ts)->destroyTime = cg.time;

  if( CG_Attached( &(*ts)->frontAttachment ) &&
      !CG_Attached( &(*ts)->backAttachment ) )
  {
    vec3_t v;

    // attach the trail head to a static point
    CG_AttachmentPoint( &(*ts)->frontAttachment, v );
    CG_SetAttachmentPoint( &(*ts)->frontAttachment, v );
    CG_AttachToPoint( &(*ts)->frontAttachment );

    (*ts)->frontAttachment.centValid = qfalse; // a bit naughty
  }

  ts = NULL;
}

/*
===============
CG_IsTrailSystemValid

Test a trail system for validity
===============
*/
qboolean CG_IsTrailSystemValid( trailSystem_t **ts )
{
  if( *ts == NULL || ( *ts && !(*ts)->valid ) )
  {
    if( *ts && !(*ts)->valid )
      *ts = NULL;

    return qfalse;
  }

  return qtrue;
}

/*
===============
CG_GarbageCollectTrailSystems

Destroy inactive trail systems
===============
*/
static void CG_GarbageCollectTrailSystems( void )
{
  int           i, j, count;
  trailSystem_t *ts;
  trailBeam_t   *tb;
  int           centNum;

  for( i = 0; i < MAX_TRAIL_SYSTEMS; i++ )
  {
    ts = &trailSystems[ i ];
    count = 0;

    //don't bother checking already invalid systems
    if( !ts->valid )
      continue;

    for( j = 0; j < MAX_TRAIL_BEAMS; j++ )
    {
      tb = &trailBeams[ j ];

      if( tb->valid && tb->parent == ts )
        count++;
    }

    if( !count )
      ts->valid = qfalse;

    //check systems where the parent cent has left the PVS
    //( local player entity is always valid )
    if( ( centNum = CG_AttachmentCentNum( &ts->frontAttachment ) ) >= 0 &&
        centNum != cg.snap->ps.clientNum )
    {
      trailSystem_t *tempTS = ts;

      if( !cg_entities[ centNum ].valid )
        CG_DestroyTrailSystem( &tempTS );
    }

    if( ( centNum = CG_AttachmentCentNum( &ts->backAttachment ) ) >= 0 &&
        centNum != cg.snap->ps.clientNum )
    {
      trailSystem_t *tempTS = ts;

      if( !cg_entities[ centNum ].valid )
        CG_DestroyTrailSystem( &tempTS );
    }

    // lifetime expired
    if( ts->destroyTime <= 0 && ts->class->lifeTime &&
        ts->birthTime + ts->class->lifeTime < cg.time )
    {
      trailSystem_t *tempTS = ts;

      CG_DestroyTrailSystem( &tempTS );
      if( cg_debugTrails.integer >= 1 )
        CG_Printf( "TS %s expired (born %d, lives %d, now %d)\n",
                   ts->class->name, ts->birthTime, ts->class->lifeTime,
                   cg.time );
    }

    if( cg_debugTrails.integer >= 1 && !ts->valid )
      CG_Printf( "TS %s garbage collected\n", ts->class->name );
  }
}

/*
===============
CG_AddTrails

Add trails to the scene
===============
*/
void CG_AddTrails( void )
{
  int           i;
  trailBeam_t   *tb;
  int           numTS = 0, numTB = 0;

  //remove expired trail systems
  CG_GarbageCollectTrailSystems( );

  for( i = 0; i < MAX_TRAIL_BEAMS; i++ )
  {
    tb = &trailBeams[ i ];

    if( tb->valid )
    {
      CG_UpdateBeam( tb );
      CG_RenderBeam( tb );
    }
  }

  if( cg_debugTrails.integer >= 2 )
  {
    for( i = 0; i < MAX_TRAIL_SYSTEMS; i++ )
      if( trailSystems[ i ].valid )
        numTS++;

    for( i = 0; i < MAX_TRAIL_BEAMS; i++ )
      if( trailBeams[ i ].valid )
        numTB++;

    CG_Printf( "TS: %d  TB: %d\n", numTS, numTB );
  }
}

static trailSystem_t  *testTS;
static qhandle_t      testTSHandle;

/*
===============
CG_DestroyTestTS_f

Destroy the test a trail system
===============
*/
void CG_DestroyTestTS_f( void )
{
  if( CG_IsTrailSystemValid( &testTS ) )
    CG_DestroyTrailSystem( &testTS );
}

/*
===============
CG_TestTS_f

Test a trail system
===============
*/
void CG_TestTS_f( void )
{
  char tsName[ MAX_QPATH ];

  if( trap_Argc( ) < 2 )
    return;

  Q_strncpyz( tsName, CG_Argv( 1 ), MAX_QPATH );
  testTSHandle = CG_RegisterTrailSystem( tsName );

  if( testTSHandle )
  {
    CG_DestroyTestTS_f( );

    testTS = CG_SpawnNewTrailSystem( testTSHandle );

    if( CG_IsTrailSystemValid( &testTS ) )
    {
      CG_SetAttachmentCent( &testTS->frontAttachment, &cg_entities[ 0 ] );
      CG_AttachToCent( &testTS->frontAttachment );
    }
  }
}
