package Math::Rotation;

use strict;
use warnings;

use Carp;
use Math::Quaternion;

use Scalar::Util;

#use Exporter;

use overload
  '='    => \&copy,
  'neg'  => \&inverse,
  '~'    => \&inverse,
  'bool' => sub { 1 },     # So we can do if ($foo=Math::Rotation->new) { .. }
  '+'    => \&multVec,
  '*'    => \&multVec,
  'eq'   => \&eq,
  '=='   => \&eq,
  'ne'   => \&ne,
  '!='   => \&ne,
  '""'   => \&stringify,
  ;

our $VERSION = '0.01';

=head1 NAME

Math::Rotation - Perl class to represent rotations

=head1 HIERARCHY

-+- L<Math::Rotation>

=head1 SEE ALSO

L<Math::Color>, L<Math::Image>, L<Math::Vec2>, L<Math::Vec3>, L<Math::Rotation>
L<Math::Quaternion>

=head1 SYNOPSIS
	
	use Math::Rotation;
	my $r = new Math::Rotation; # Make a new unit rotation

	# Make a rotation about the axis (0,1,0)
	my $r2 = new Math::Rotation([0,1,0], 0.1);
	my $r3 = new Math::Rotation(1, 2, 3, 4);

	my $fromVector = [1,2,3];
	my $toVector = [2,3,4];
	my $r4 = new Math::Rotation($fromVector, $toVector);

	my $r5 = $r2 + $r3;
	my $r6 = -$r5;

=head1 DESCRIPTION

=head1 METHODS

=over 1

=item B<new>

	# Make a new unit rotation.
	my $r = new Math::Rotation;

	my $r2 = new Math::Rotation(1,2,3,4);         # (x,y,z, angle)
	my $r3 = new Math::Rotation([1,2,3],4);       # (axis, angle)
	my $r3 = new Math::Rotation([1,2,3],[1,2,3]); # (fromVec, toVec)

=cut

sub new {
	my $self  = shift;
	my $class = ref($self) || $self;
	my $this  = bless {}, $class;

	if ( 0 == @_ ) {
		# No arguments, default to standard rotation.
		$this->{axis}       = [ 0, 0, 1 ];
		$this->{angle}      = 0;
		$this->{quaternion} = new Math::Quaternion();
	} elsif ( 1 == @_ ) {    # rotation
		$this->{axis}       = [ $_[0]->getAxis ];
		$this->{angle}      = $_[0]->getAngle;
		$this->{quaternion} = $_[0]->getQuaternion;
	} elsif ( 2 == @_ ) {

		my $arg1    = shift;
		my $reftype = ref($arg1);

		if ( "ARRAY" eq $reftype ) {
			my $arg2    = shift;
			my $reftype = ref($arg2);

			if ( !$reftype ) {    # vec1, angle

				$this->setAxisAngle( @$arg1, $arg2 );

			} elsif ( "ARRAY" eq $reftype ) {    # vec1, vec2
				$this->private::setQuaternion( eval { Math::Quaternion::rotation( $arg1, $arg2 ) }
					  || new Math::Quaternion() );
			} else {
				croak("Don't understand arguments passed to new()");
			}
		} else {
			croak("Don't understand arguments passed to new()");
		}
	} elsif ( 4 == @_ ) {    # x,y,z,angle
		$this->setAxisAngle(@_);
	} else {
		croak("Don't understand arguments passed to new()");
	}

	return $this;
}

=item B<new_from_quaternion>
	$r5 = new_from_quaternion Math::Rotation(new Math::Quaternion);
=cut

sub new_from_quaternion {
	return $_[0]->private::new_from_quaternion( new Math::Quaternion( $_[1] ) );
}

sub private::new_from_quaternion {
	my $self  = shift;
	my $class = ref($self) || $self;
	my $this  = bless {}, $class;

	$this->private::setQuaternion(shift);

	return $this;
}

=item B<copy>

Makes a copy of the rotation
	
	$r2 = $r1->copy;
	$r2 = new Math::Rotation($r1);
=cut

sub copy {
	my $this = shift;
	return $this->new($this);
}

=item B<setX(x)>

Sets the first value of the axis vector

	$r->setX(1);
=cut

sub setX {
	my $this = shift;
	$this->{axis}->[0] = shift;
	$this->{quaternion} = Math::Quaternion::rotation( $this->{angle}, $this->{axis} );
}

=item B<setY(y)>

Sets the second value of the axis vector

	$r->setY(2);
=cut

sub setY {
	my $this = shift;
	$this->{axis}->[1] = shift;
	$this->{quaternion} = Math::Quaternion::rotation( $this->{angle}, $this->{axis} );
}

=item B<setZ(z)>

Sets the third value of the axis vector

	$r->setZ(3);
=cut

sub setZ {
	my $this = shift;
	$this->{axis}->[2] = shift;
	$this->{quaternion} = Math::Quaternion::rotation( $this->{angle}, $this->{axis} );
}

=item B<setAxis(x,y,z)>

Sets axis of rotation from a 3 components @array.

	$r->setAxis(1,2,3);
=cut

sub setAxis {
	my $this = shift;
	@{ $this->{axis} } = @_;
	$this->{quaternion} = Math::Quaternion::rotation( $this->{angle}, $this->{axis} );
}

=item B<setAngle(angle)>

Sets angle of rotation in radiants.

	$r->setAngle(4);
=cut

sub setAngle {
	my $this = shift;
	$this->{angle} = shift;
	$this->{quaternion} = Math::Quaternion::rotation( $this->{angle}, $this->{axis} );
}

=item B<setAxisAngle(x,y,z, angle)>
=cut
=item B<setValue(x,y,z, angle)>

Sets value of rotation from axis angle.

	$r->setAxisAngle(1,2,3,4);
	$r->setValue(1,2,3,4);
=cut

sub setAxisAngle {
	my $this = shift;
	$this->{axis}       = [ @_[ 0, 1, 2 ] ];
	$this->{angle}      = $_[3];
	$this->{quaternion} = Math::Quaternion::rotation( $this->{angle}, $this->{axis} );
}
*setValue = \&setAxisAngle;

=item B<setQuaternion(quaternion)>

Sets value of rotation from a quaternion.

	$r->setQuaternion(new Math::Quaternion(1,2,3,4));
=cut

sub setQuaternion {
	my $this = shift;
	$this->private::setQuaternion( eval { new Math::Quaternion(shift) } || new Math::Quaternion() );
}

sub private::setQuaternion {
	my $this = shift;
	my $q    = shift;
	$this->{quaternion} = $q;
	@{ $this->{axis} } = $q->rotation_axis;
	$this->{angle} = $q->rotation_angle;
}

=item B<getX>

Returns the first value of the axis vector.

	$x = $r->getX;
=cut

sub getX { $_[0]->{axis}->[0] }

=item B<getY>

Returns the second value of the axis vector.

	$y = $r->getY;
=cut

sub getY { $_[0]->{axis}->[1] }

=item B<getZ>

Returns the third value of the axis vector

	$z = $r->getZ;
=cut

sub getZ { $_[0]->{axis}->[2] }

=item B<getAxis>

Returns the axis of rotation as an @array.

	@axis = $r->getAxis;
=cut

sub getAxis { @{ $_[0]->{axis} } }

=item B<getAngle>

Returns corresponding 3D rotation angle in radians.

	$angle = $r->getAngle;
=cut

sub getAngle { $_[0]->{angle} }

=item B<getAxisAngle>
=cut
=item B<getValue>

Returns corresponding 3D rotation (x, y, z, angle).

	($x, $y, $z, $angle) = $r->getAxisAngle;
	($x, $y, $z, $angle) = $r->getValue;
	
	$v4 = [ $r->getValue ];

=cut

sub getAxisAngle {
	my $this = shift;
	return (
		$this->getAxis,
		$this->getAngle
	);
}
*getValue = \&getAxisAngle;

=item B<getQuaternion>

Returns corresponding Math::Quaternion.
	
	$q = $r->getQuaternion;
=cut

sub getQuaternion { new Math::Quaternion( $_[0]->{quaternion} ) }

=item B<inverse>

Returns a Math::Rotation object whose value is the inverse of this object's rotation.
	
	$i = $r->inverse;
	$i = -$r;
	$i = ~$r;
=cut

# (1,2,3, 4) -> (1,2,3, -4)
sub inverse { $_[0]->private::new_from_quaternion( $_[0]->{quaternion}->inverse ) }

=item B<multiply(rotation)>

Returns an Math::Rotation whose value is the object multiplied by the passed Math::Rotation.
	
	$r = $r1->multiply($r2);
	$r = $r1 * $r2;

	$r1 *= $r1;

	$r = $r1 + $r2;
=cut

sub multiply { $_[0]->private::new_from_quaternion( $_[1]->{quaternion}->multiply( $_[0]->{quaternion} ) ) }

=item B<multVec(x,y,z) or multVec([x,y,z])>

Returns an $array whose value is the 3D vector [x,y,z] multiplied by the matrix corresponding to this object's rotation.

	$v = $r->multVec([1,2,3]);
	@v = $r->multVec(1,2,3);

	$v = $r1 * [1,2,3];
	$v = $r1 * Math::Vec3->new(1,2,3);

	$v = $r->multVec(new Math::Vec3(1,2,3));
	ref($v) eq "Math::Vec3";
=cut

sub multVec {
	my $this = shift;
	if ( @_ < 3 ) {
		if ( Scalar::Util::reftype( $_[0] ) eq "ARRAY" ) {
			my @v = $this->{quaternion}->rotate_vector( @{ $_[0] } );
			return ref( $_[0] ) eq "ARRAY" ? [@v] : $_[0]->new(@v);
		} else {
			return $this->multiply( $_[0] );
		}
	} elsif ( @_ < 4 ) {
		return $this->{quaternion}->rotate_vector(@_);
	} else {
		return $this->multiply( $this->new(@_) );
	}
}

=item B<slerp(destRotation, t)>

Returns a Math::Rotation object whose value is the spherical linear interpolation between this object's
rotation and destRotation at value 0 <= t <= 1. For t = 0, the value is this object's rotation.
For t = 1, the value is destRotation.

	$r = $r1->slerp($r2, $_) foreach map {$_/10} (1..10);
=cut

sub slerp { $_[0]->private::new_from_quaternion( $_[0]->{quaternion}->slerp( $_[1]->{quaternion}, $_[2] ) ) }

=item B<eq(rotation)>

	my $bool = $r1->eq($r2);
	my $bool = $r1 eq $r2;
	my $bool = $r1 == $r2;
=cut

sub eq {
	my ( $a, $b ) = @_;
	return "$a" eq $b;
}

=item B<ne(rotation)>

	my $bool = $r1->ne($r2);
	my $bool = $r1 ne $r2;
	my $bool = $r1 != $r2;
=cut

sub ne {
	my ( $a, $b ) = @_;
	return "$a" ne $b;
}

=item B<stringify>

Returns a string representation of the rotation. This is used
to overload the '""' operator, so that rotations may be
freely interpolated in strings.

	my $q = new Math::Rotation(1,2,3,4);
	print $q->stringify;                # "1 2 3 4"
	print "$q";                         # "1 2 3 4"

=cut

sub stringify {
	my $this = shift;
	return join " ", $this->getAxisAngle;
}

1;
__END__
