/*
smd.c - Studio Model Data format writer
Copyright (C) 2020 Andrey Akhmichin

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 3 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.
*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "const.h"
#include "com_model.h"
#include "xash3d_mathlib.h"
#include "crtlib.h"
#include "studio.h"
#include "mdldec.h"
#include "smd.h"
#include "utils.h"

static matrix3x4	*bonetransform;
static matrix3x4	*worldtransform;

/*
============
CreateBoneTransformMatrices
============
*/
static qboolean CreateBoneTransformMatrices( matrix3x4 **matrix )
{
	*matrix = calloc( model_hdr->numbones, sizeof( matrix3x4 ) );

	if( !*matrix )
	{
		fputs( "ERROR: Couldn't allocate memory for bone transformation matrices!\n", stderr );
		return false;
	}

	return true;
}

/*
============
FillBoneTransformMatrices
============
*/
static void FillBoneTransformMatrices( void )
{
	int		 i;
	mstudiobone_t	*bone = (mstudiobone_t *)( (byte *)model_hdr + model_hdr->boneindex );
	matrix3x4	 bonematrix;
	vec4_t		 q;

	for( i = 0; i < model_hdr->numbones; ++i, ++bone )
	{
		AngleQuaternion( &bone->value[3], q, true );
		Matrix3x4_FromOriginQuat( bonematrix, q, bone->value );

		if( bone->parent == -1 )
		{
			Matrix3x4_Copy( bonetransform[i], bonematrix );
			continue;
		}

		Matrix3x4_ConcatTransforms( bonetransform[i], bonetransform[bone->parent], bonematrix );
	}
}

/*
============
FillWorldTransformMatrices
============
*/
static void FillWorldTransformMatrices( void )
{
	int			 i;
	mstudioboneinfo_t	*boneinfo = (mstudioboneinfo_t *)( (byte *)model_hdr + model_hdr->boneindex + model_hdr->numbones * sizeof( mstudiobone_t ) );

	for( i = 0; i < model_hdr->numbones; ++i, ++boneinfo )
		Matrix3x4_ConcatTransforms( worldtransform[i], bonetransform[i], boneinfo->poseToBone );
}

/*
============
RemoveBoneTransformMatrices
============
*/
static void RemoveBoneTransformMatrices( matrix3x4 **matrix )
{
	free( *matrix );
}

/*
============
ClipRotations
============
*/
static void ClipRotations( vec3_t angle )
{
	int i;

	for( i = 0; i < 3; i++ )
	{
		while( angle[i] >= M_PI_F )
			angle[i] -= M_PI2_F;

		while( angle[i] < -M_PI_F )
			angle[i] += M_PI2_F;
	}
}

/*
============
ProperBoneRotationZ
============
*/
static void ProperBoneRotationZ( vec_t *motion, float angle )
{
	float	tmp, rot;

	rot = DEG2RAD( angle );

	tmp = motion[0];
	motion[0] = motion[1];
	motion[1] = -tmp;

	motion[5] += rot;
}

/*
============
CalcBonePosition
============
*/
static void CalcBonePosition( mstudioanim_t *anim, mstudiobone_t *bone, vec_t *motion, int frame )
{
	int			 i, j;
	float			 value;
	mstudioanimvalue_t	*animvalue;

	for( i = 0; i < 6; i++ )
	{
		motion[i] = bone->value[i];

		if( !anim->offset[i] )
			continue;

		animvalue = (mstudioanimvalue_t *)( (byte *)anim + anim->offset[i] );

		j = frame;

		while( animvalue->num.total <= j )
		{
			j -= animvalue->num.total;
			animvalue += animvalue->num.valid + 1;
		}

		if( animvalue->num.valid > j )
			value = animvalue[j + 1].value;
		else
			value = animvalue[animvalue->num.valid].value;

		motion[i] += value * bone->scale[i];
	}
}

/*
============
WriteNodes
============
*/
static void WriteNodes( FILE *fp )
{
	int		 i;
	mstudiobone_t	*bone = (mstudiobone_t *)( (byte *)model_hdr + model_hdr->boneindex );

	fputs( "nodes\n", fp );

	for( i = 0; i < model_hdr->numbones; ++i, ++bone )
		fprintf( fp, "%3i \"%s\" %i\n", i, bone->name, bone->parent );

	fputs( "end\n", fp );
}

/*
============
WriteSkeleton
============
*/
static void WriteSkeleton( FILE *fp )
{
	int		 i, j;
	mstudiobone_t	*bone = (mstudiobone_t *)( (byte *)model_hdr + model_hdr->boneindex );

	fputs( "skeleton\n", fp );
	fputs( "time 0\n", fp );

	for( i = 0; i < model_hdr->numbones; ++i, ++bone )
	{
		fprintf( fp, "%3i", i );

		for( j = 0; j < 6; j++ )
			fprintf( fp, " %f", bone->value[j] );

		fputs( "\n", fp );
	}

	fputs( "end\n", fp );
}

/*
============
WriteTriangleInfo
============
*/
static void WriteTriangleInfo( FILE *fp, mstudiomodel_t *model, mstudiotexture_t *texture, mstudiotrivert_t **triverts, qboolean isevenstrip )
{
	int			 i, j, k, l, index;
	int			 vert_index;
	int			 norm_index;
	int			 bone_index;
	int			 valid_bones;
	float			 s, t, u, v;
	byte			*vertbone;
	vec3_t			*studioverts;
	vec3_t			*studionorms;
	vec3_t			 vert, norm;
	float			 weights[MAXSTUDIOBONEWEIGHTS], oldweight, totalweight;
	matrix3x4		 bonematrix[MAXSTUDIOBONEWEIGHTS], skinmatrix, *pskinmatrix;
	mstudioboneweight_t	*studioboneweights;

	vertbone    = ( (byte *)model_hdr + model->vertinfoindex );
	studioverts = (vec3_t *)( (byte *)model_hdr + model->vertindex );
	studionorms = (vec3_t *)( (byte *)model_hdr + model->normindex );
	studioboneweights = (mstudioboneweight_t *)( (byte *)model_hdr + model->blendvertinfoindex );

	s = 1.0f / texture->width;
	t = 1.0f / texture->height;

	fprintf( fp, "%s\n", texture->name );

	for( i = 0; i < 3; i++ )
	{
		index = isevenstrip ? ( i + 1 ) % 3 : i;
		vert_index = triverts[index]->vertindex;
		norm_index = triverts[index]->normindex;
		bone_index = vertbone[vert_index];

		if( model_hdr->flags & STUDIO_HAS_BONEWEIGHTS )
		{
			valid_bones = 0, totalweight = 0;
			memset( skinmatrix, 0, sizeof( matrix3x4 ) );

			for( j = 0; j < MAXSTUDIOBONEWEIGHTS; ++j )
				if( studioboneweights[vert_index].bone[j] != -1 )
					valid_bones++;

			for( j = 0; j < valid_bones; ++j )
			{
				Matrix3x4_Copy( bonematrix[j], worldtransform[studioboneweights[vert_index].bone[j]] );
				weights[j] = studioboneweights[vert_index].weight[j] / 255.0f;
				totalweight += weights[j];
			}

			oldweight = weights[0];

			if( totalweight < 1.0f )
				weights[0] += 1.0f - totalweight;

			for( j = 0; j < valid_bones; ++j )
				for( k = 0; k < 3; ++k )
					for( l = 0; l < 4; ++l )
						skinmatrix[k][l] += bonematrix[j][k][l] * weights[j];

			pskinmatrix = &skinmatrix;
		}
		else
			pskinmatrix = &bonetransform[bone_index];

		Matrix3x4_VectorTransform( *pskinmatrix, studioverts[vert_index], vert );
		Matrix3x4_VectorRotate( *pskinmatrix, studionorms[norm_index], norm );
		VectorNormalize( norm );

		if( texture->flags & STUDIO_NF_UV_COORDS )
		{
			u = HalfToFloat( triverts[index]->s );
			v = -HalfToFloat( triverts[index]->t );
		}
		else
		{
			u = ( triverts[index]->s + 1.0f ) * s;
			v = 1.0f - triverts[index]->t * t;
		}

		fprintf( fp, "%3i %f %f %f %f %f %f %f %f",
		    bone_index,
		    vert[0], vert[1], vert[2],
		    norm[0], norm[1], norm[2],
		    u, v );

		if( model_hdr->flags & STUDIO_HAS_BONEWEIGHTS )
		{
			fprintf( fp, " %d", valid_bones );

			weights[0] = oldweight;

			for( j = 0; j < valid_bones; ++j )
				fprintf( fp, " %d %f",
				    studioboneweights[vert_index].bone[j],
				    weights[j] );
		}

		fputs( "\n", fp );
	}
}

/*
============
WriteTriangles
============
*/
static void WriteTriangles( FILE *fp, mstudiomodel_t *model )
{
	int			 i, j, k;
	mstudiomesh_t		*mesh = (mstudiomesh_t *)( (byte *)model_hdr + model->meshindex );
	mstudiotexture_t	*texture;
	mstudiotrivert_t	*triverts[3];
	short			*tricmds;

	fputs( "triangles\n", fp );

	for( i = 0; i < model->nummesh; ++i, ++mesh )
	{
		tricmds = (short *)( (byte *)model_hdr + mesh->triindex );
		texture = (mstudiotexture_t *)( (byte *)texture_hdr + texture_hdr->textureindex ) + mesh->skinref;

		while( ( j = *( tricmds++ ) ) )
		{
			if( j >= 0 )
			{
				// triangle strip
				for( k = 0; j > 0; j--, k++, tricmds += 4 )
				{
					if( k == 0 )
					{
						triverts[0] = (mstudiotrivert_t *)tricmds;
					}
					else if( k == 1 )
					{
						triverts[2] = (mstudiotrivert_t *)tricmds;
					}
					else if( k == 2 )
					{
						triverts[1] = (mstudiotrivert_t *)tricmds;

						WriteTriangleInfo( fp, model, texture, triverts, true );
					}
					else if( k % 2 )
					{
						triverts[0] = triverts[2];
						triverts[2] = (mstudiotrivert_t *)tricmds;

						WriteTriangleInfo( fp, model, texture, triverts, false );
					}
					else
					{
						triverts[0] = triverts[1];
						triverts[1] = (mstudiotrivert_t *)tricmds;

						WriteTriangleInfo( fp, model, texture, triverts, true );
					}
				}
			}
			else
			{
				// triangle fan
				j = abs( j );

				for( k = 0; j > 0; j--, k++, tricmds += 4 )
				{
					if( k == 0 )
					{
						triverts[0] = (mstudiotrivert_t *)tricmds;
					}
					else if( k == 1 )
					{
						triverts[2] = (mstudiotrivert_t *)tricmds;
					}
					else if( k == 2 )
					{
						triverts[1] = (mstudiotrivert_t *)tricmds;

						WriteTriangleInfo( fp, model, texture, triverts, false );
					}
					else
					{
						triverts[2] = triverts[1];
						triverts[1] = (mstudiotrivert_t *)tricmds;

						WriteTriangleInfo( fp, model, texture, triverts, false );
					}
				}
			}
		}
	}

	fputs( "end\n", fp );
}

/*
============
WriteFrameInfo
============
*/
static void WriteFrameInfo( FILE *fp, mstudioanim_t *anim, mstudioseqdesc_t *seqdesc, int frame )
{
	int			 i, j;
	float			 scale;
	vec_t			 motion[6]; // x, y, z, xr, yr, zr
	mstudiobone_t		*bone = (mstudiobone_t *)( (byte *)model_hdr + model_hdr->boneindex );

	fprintf( fp, "time %i\n", frame );

	for( i = 0; i < model_hdr->numbones; ++i, ++anim, ++bone )
	{
		CalcBonePosition( anim, bone, motion, frame );

		if( bone->parent == -1 )
		{
			if( seqdesc->numframes > 1 && frame > 0 )
			{
				scale = frame / (float)( seqdesc->numframes - 1 );

				VectorMA( motion, scale, seqdesc->linearmovement, motion );
			}

			ProperBoneRotationZ( motion, 270.0f );
		}

		ClipRotations( &motion[3] );

		fprintf( fp, "%3i  ", i );

		for( j = 0; j < 6; j++ )
			fprintf( fp, " %f", motion[j] );

		fputs( "\n", fp );
	}
}

/*
============
WriteAnimations
============
*/
static void WriteAnimations( FILE *fp, mstudioseqdesc_t *seqdesc, int blend )
{
	int		 i;
	mstudioanim_t	*anim;

	fputs( "skeleton\n", fp );

	anim = (mstudioanim_t *)( (byte *)anim_hdr[seqdesc->seqgroup] + seqdesc->animindex );
	anim += blend * model_hdr->numbones;

	for( i = 0; i < seqdesc->numframes; i++ )
		WriteFrameInfo( fp, anim, seqdesc, i );

	fputs( "end\n", fp );
}

/*
============
WriteReferences
============
*/
static void WriteReferences( void )
{
	int			 i, j;
	int			 len;
	FILE			*fp;
	mstudiomodel_t		*model;
	mstudiobodyparts_t	*bodypart;
	char			 filename[MAX_SYSPATH];

	if( !CreateBoneTransformMatrices( &bonetransform ) )
		return;

	FillBoneTransformMatrices();

	if( model_hdr->flags & STUDIO_HAS_BONEINFO )
	{
		if( !CreateBoneTransformMatrices( &worldtransform ) )
			return;

		FillWorldTransformMatrices();
	}

	bodypart = (mstudiobodyparts_t *)( (byte *)model_hdr + model_hdr->bodypartindex );

	for( i = 0; i < model_hdr->numbodyparts; ++i, ++bodypart )
	{
		model = (mstudiomodel_t *)( (byte *)model_hdr + bodypart->modelindex );

		for( j = 0; j < bodypart->nummodels; ++j, ++model )
		{
			if( !Q_strncmp( model->name, "blank", 5 ) )
				continue;

			len = Q_snprintf( filename, MAX_SYSPATH, "%s%s.smd", destdir, model->name );

			if( len == -1 )
			{
				fprintf( stderr, "ERROR: Destination path is too long. Couldn't write %s.smd\n", model->name );
				goto _fail;
			}

			fp = fopen( filename, "w" );

			if( !fp )
			{
				fprintf( stderr, "ERROR: Couldn't write %s\n", filename );
				goto _fail;
			}

			fputs( "version 1\n", fp );

			WriteNodes( fp );
			WriteSkeleton( fp );
			WriteTriangles( fp, model );

			fclose( fp );

			printf( "Reference: %s\n", filename );
		}
	}

_fail:
	RemoveBoneTransformMatrices( &bonetransform );

	if( model_hdr->flags & STUDIO_HAS_BONEINFO )
		RemoveBoneTransformMatrices( &worldtransform );
}

/*
============
WriteSequences
============
*/
static void WriteSequences( void )
{
	int			 i, j;
	int			 len, namelen, emptyplace;
	FILE			*fp;
	char			 path[MAX_SYSPATH];
	mstudioseqdesc_t	*seqdesc = (mstudioseqdesc_t *)( (byte *)model_hdr + model_hdr->seqindex );

	len = Q_snprintf( path, MAX_SYSPATH, "%s" DEFAULT_SEQUENCEPATH, destdir );

	if( len == -1 || !MakeDirectory( path ))
	{
		fputs( "ERROR: Destination path is too long or write permission denied. Couldn't create directory for sequences\n", stderr );
		return;
	}

	emptyplace = MAX_SYSPATH - len;

	for( i = 0; i < model_hdr->numseq; ++i, ++seqdesc )
	{
		for( j = 0; j < seqdesc->numblends; j++ )
		{
			if( seqdesc->numblends == 1 )
				namelen = Q_snprintf( &path[len], emptyplace, "%s.smd", seqdesc->label );
			else
				namelen = Q_snprintf( &path[len], emptyplace, "%s_blend%02i.smd", seqdesc->label, j + 1 );

			if( namelen == -1 )
			{
				fprintf( stderr, "ERROR: Destination path is too long. Couldn't write %s.smd\n", seqdesc->label );
				return;
			}

			fp = fopen( path, "w" );

			if( !fp )
			{
				fprintf( stderr, "ERROR: Couldn't write %s\n", path );
				return;
			}

			fputs( "version 1\n", fp );

			WriteNodes( fp );
			WriteAnimations( fp, seqdesc, j );

			fclose( fp );

			printf( "Sequence: %s\n", path );
		}
	}
}

void WriteSMD( void )
{
	WriteReferences();
	WriteSequences();
}

