package MD3;
use strict;
use warnings;
use Math::Trig;

sub new($)
{
	my ($class) = @_;
	return bless
	{
		filename => "",
		flags => 0, # MF_
		surfaces => [],
		mins => undef,
		maxs => undef,
		scale => 1,
		offset => [0, 0, 0]
	}, $class;
}

# util functions
sub idxpush($$)
{
	my ($arr, $item) = @_;
	my $ret = @$arr;
	push @$arr, $item;
	return $ret;
}

sub max($$)
{
	my ($a, $b) = @_;
	return $a if $a >= $b;
	return $b;
}

sub min($$)
{
	my ($a, $b) = @_;
	return $a if $a <= $b;
	return $b;
}

sub AddSurface($$)
{
	my ($md3, $shadername) = @_;
	my $idx = scalar @{$md3->{surfaces}};
	idxpush $md3->{surfaces},
	{
		triangles => [],
		shaders => [],
		xyznormal => [],
		shaders => [$shadername]
	};
}

sub AddVertex($$$$$$$$$$)
{
	my ($md3, $surf, $x, $y, $z, $nx, $ny, $nz, $s, $t) = @_;
	if(defined $md3->{mins})
	{
		$md3->{mins}[0] = min($md3->{mins}[0], $x);
		$md3->{mins}[1] = min($md3->{mins}[1], $y);
		$md3->{mins}[2] = min($md3->{mins}[2], $z);
		$md3->{maxs}[0] = max($md3->{maxs}[0], $x);
		$md3->{maxs}[1] = max($md3->{maxs}[1], $y);
		$md3->{maxs}[2] = max($md3->{maxs}[2], $z);
	}
	else
	{
		$md3->{mins} = [$x, $y, $z];
		$md3->{maxs} = [$x, $y, $z];
	}
	idxpush $md3->{surfaces}->[$surf]->{xyznormal},
	[
		$x, $y, $z,
		$nx, $ny, $nz,
		$s, $t
	];
}

sub AddTriangle($$$$$)
{
	my ($md3, $surf, $a, $b, $c) = @_;
	idxpush $md3->{surfaces}->[$surf]->{triangles},
	[
		$a, $b, $c
	];
}

sub U8($)   { return pack "C",   @_; }
sub S16($)  { return pack "s",   @_; }
sub S32($)  { return pack "l",   @_; }
sub F32($)  { return pack "f",   @_; }
sub VEC3($) { return pack "fff", @_; }
sub ZeroPad($$)
{
	my ($len, $data) = @_;
	return substr($data . ("\0" x $len), 0, $len);
}

sub BufNew()
{
	return [""];
}
sub BufPos($)
{
	my ($buf) = @_;
	return length $buf->[0];
}
sub BufAppend($$)
{
	my ($buf, $data) = @_;
	$buf->[0] .= $data;
}
sub BufWrite($$$)
{
	my ($buf, $ofs, $data) = @_;
	substr $buf->[0], $ofs, length($data), $data;
}
sub BufReturn($)
{
	my ($buf) = @_;
	return $buf->[0];
}

sub sphereorigin(@@)
{
	my ($minx, $miny, $minz, $maxx, $maxy, $maxz) = @_;
	return (0, 0, 0);
}

sub sphereradius(@@)
{
	my ($minx, $miny, $minz, $maxx, $maxy, $maxz) = @_;
	my $x = max(abs($minx), abs($minx));
	my $y = max(abs($miny), abs($miny));
	my $z = max(abs($minz), abs($minz));
	return sqrt($x*$x + $y*$y + $z*$z);
}

sub ApplyInverseScale($$)
{
	my ($md3, $scale) = @_;
	$md3->{scale} = $scale;
}

sub ApplyInverseOffset($$$$)
{
	my ($md3, @offset) = @_;
	$md3->{offset} = \@offset;
}

sub ModelOffset($)
{
	my ($md3) = @_;
	my $scale = 1;
	my $mins = $md3->{mins};
	my $maxs = $md3->{maxs};

	die "Empty model, you make no sense"
		if not defined $mins;

	my @offset = map { int(0.5 + (0.5 * $_) / 32) * 32 } (
		$mins->[0] + $maxs->[0],
		$mins->[1] + $maxs->[1],
		$mins->[2] + $maxs->[2]
	);

	return @offset;
}

sub ModelScale($)
{
	my ($md3) = @_;
	my $scale = 1;
	my $mins = $md3->{mins};
	my $maxs = $md3->{maxs};
	my @offset = @{$md3->{offset}};

	die "Empty model, you make no sense"
		if not defined $mins;

	my $highest = [sort { $a <=> $b }
		abs($mins->[0] - $offset[0]),
		abs($mins->[1] - $offset[1]),
		abs($mins->[2] - $offset[2]),
		abs($maxs->[0] - $offset[0]),
		abs($maxs->[1] - $offset[1]),
		abs($maxs->[2] - $offset[2])
	]->[-1];

	#$scale = $highest / (16384.0 / 64.0); # play it safe, use powers of two
	#$scale = int($scale * 16 + 0.5) / 16;
	$scale = 1;
	$scale *= 2
		while $highest / $scale >= 511;
	die "Too high model scale"
		if $scale >= 16;
	$scale = 1
		if $scale <= 1;
	return $scale;
}

sub Write($)
{
	my ($md3) = @_;
	my $data = BufNew();

	my $MD3_START = BufPos $data;
	BufAppend $data, "IDP3";
	BufAppend $data, S32 15;
	BufAppend $data, ZeroPad 64, $md3->{filename};
	BufAppend $data, S32 $md3->{flags};
	BufAppend $data, S32 1; # always 1 frame
	BufAppend $data, S32 0; # always 0 tags
	BufAppend $data, S32 scalar @{$md3->{surfaces}};
	BufAppend $data, S32 0; # skins???

	my $OFS_OFS_FRAMES_MD3_START = BufPos $data;
	BufAppend $data, S32 0xDEADBEEF;

	my $OFS_OFS_TAGS_MD3_START = BufPos $data;
	BufAppend $data, S32 0xDEADBEEF;

	my $OFS_OFS_SURFACES_MD3_START = BufPos $data;
	BufAppend $data, S32 0xDEADBEEF;

	my $OFS_OFS_EOF_MD3_START = BufPos $data;
	BufAppend $data, S32 0xDEADBEEF;

	my $FRAMES_START = BufPos $data;
	BufWrite $data, $OFS_OFS_FRAMES_MD3_START, S32($FRAMES_START - $MD3_START);
		my @mins = map { ($md3->{mins}->[$_] - $md3->{offset}->[$_]) / $md3->{scale} } 0..2;
		my @maxs = map { ($md3->{maxs}->[$_] - $md3->{offset}->[$_]) / $md3->{scale} } 0..2;
		BufAppend $data, VEC3 @mins;
		BufAppend $data, VEC3 @maxs;
		BufAppend $data, VEC3(sphereorigin @mins, @maxs);
		BufAppend $data, F32(sphereradius @mins, @maxs);
		BufAppend $data, ZeroPad 16, "Some Frame";

	my $TAGS_START = BufPos $data;
	BufWrite $data, $OFS_OFS_TAGS_MD3_START, S32($TAGS_START - $MD3_START);

	my $SURFACES_START = BufPos $data;
	BufWrite $data, $OFS_OFS_SURFACES_MD3_START, S32($SURFACES_START - $MD3_START);
		for my $surf(@{$md3->{surfaces}})
		{
			my $SURFACE_START = BufPos $data;
			BufAppend $data, "IDP3";
			BufAppend $data, ZeroPad 64, "Some Surface";
			BufAppend $data, S32 0; # flags?
			BufAppend $data, S32 1; # always 1 frame
			BufAppend $data, S32 scalar @{$surf->{shaders}};
			BufAppend $data, S32 scalar @{$surf->{xyznormal}};
			BufAppend $data, S32 scalar @{$surf->{triangles}};

			my $OFS_OFS_TRIANGLES_SURFACE_START = BufPos $data;
			BufAppend $data, S32 0xDEADBEEF;

			my $OFS_OFS_SHADERS_SURFACE_START = BufPos $data;
			BufAppend $data, S32 0xDEADBEEF;

			my $OFS_OFS_ST_SURFACE_START = BufPos $data;
			BufAppend $data, S32 0xDEADBEEF;

			my $OFS_OFS_XYZNORMAL_SURFACE_START = BufPos $data;
			BufAppend $data, S32 0xDEADBEEF;

			my $OFS_OFS_END_SURFACE_START = BufPos $data;
			BufAppend $data, S32 0xDEADBEEF;

			my $SHADERS_START = BufPos $data;
			BufWrite $data, $OFS_OFS_SHADERS_SURFACE_START, S32($SHADERS_START - $SURFACE_START);
				for my $i(0..@{$surf->{shaders}} - 1)
				{
					BufAppend $data, ZeroPad 64, $surf->{shaders}->[$i];
					BufAppend $data, S32 $i;
				}

			my $TRIANGLES_START = BufPos $data;
			BufWrite $data, $OFS_OFS_TRIANGLES_SURFACE_START, S32($TRIANGLES_START - $SURFACE_START);
				for my $triangle(@{$surf->{triangles}})
				{
					BufAppend $data, S32 $triangle->[0];
					BufAppend $data, S32 $triangle->[1];
					BufAppend $data, S32 $triangle->[2];
				}

			my $ST_START = BufPos $data;
			BufWrite $data, $OFS_OFS_ST_SURFACE_START, S32($ST_START - $SURFACE_START);
				for my $xyznormal(@{$surf->{xyznormal}})
				{
					BufAppend $data, F32 $xyznormal->[6];
					BufAppend $data, F32 $xyznormal->[7];
				}

			my $XYZNORMAL_START = BufPos $data;
			BufWrite $data, $OFS_OFS_XYZNORMAL_SURFACE_START, S32($XYZNORMAL_START - $SURFACE_START);
				for my $xyznormal(@{$surf->{xyznormal}})
				{
					BufAppend $data, S16(($xyznormal->[0] - $md3->{offset}->[0]) * 64.0 / $md3->{scale});
					BufAppend $data, S16(($xyznormal->[1] - $md3->{offset}->[1]) * 64.0 / $md3->{scale});
					my $v = ($xyznormal->[2] - $md3->{offset}->[2]) * 64.0 / $md3->{scale};
					die "$xyznormal->[2], $md3->{offset}->[2], $md3->{scale} -> $v" if $v < -32768 or $v > 32767;
					BufAppend $data, S16(($xyznormal->[2] - $md3->{offset}->[2]) * 64.0 / $md3->{scale});
					my $azimuth = int(atan2($xyznormal->[4], $xyznormal->[3]) * 128 / pi) & 0xFF;
					my $zenith = int(acos($xyznormal->[5]) * 128 / pi + 0.5) & 0xFF;
					
					BufAppend $data, U8 $zenith;
					BufAppend $data, U8 $azimuth;
				}

			my $END = BufPos $data;
			BufWrite $data, $OFS_OFS_END_SURFACE_START, S32($END - $SURFACE_START);
		}
	
	my $EOF = BufPos $data;
	BufWrite $data, $OFS_OFS_EOF_MD3_START, S32($EOF - $MD3_START);

	return BufReturn $data;
}

1;
