use strict;
use warnings;
use Box2D;
use SDL;
use SDL::Video;
use SDL::Events qw(:type);
use SDLx::App;

# pixels
my $width  = 400;
my $height = 400;

# pixels per meter
my $ppm = 30;

# meters per pixel
my $mpp = 1.0 / $ppm;

# frames per second
my $fps      = 60.0;
my $timestep = 1.0 / $fps;

# velocity iterations
my $vIters = 30;

# position iterations
my $pIters = 30;

my $gravity = make_vec2( 0.0, 10.0 );
my $world = Box2D::b2World->new( $gravity, 1 );

my $ground = make_ground();

my $walls = make_walls(
    [ [ s2w(0.0), s2w(0.0) ], [ s2w(0.0), s2w($height) ] ],
    [ [ s2w(0.0), s2w(0.0) ], [ s2w($width), s2w(0.0) ] ],
    [ [ s2w($width), s2w(0.0) ], [ s2w($width), s2w($height) ] ],
    [ [ s2w(0.0), s2w($height) ], [ s2w($width), s2w($height) ] ],
);

my $ball = make_ball(
    x       => s2w( $width / 2.0 ),
    y       => s2w( $height / 2.0 ),
    radius  => s2w( rand(5.0) + 10.0 ),
    color   => [ 255, 0, 0 ],
    density => 1.0,
);

my @balls;
for ( 1 .. 100 ) {
    my $b = make_ball(
        x       => rand s2w($width),
        y       => rand s2w($height),
        radius  => s2w( rand(5.0) + 10.0 ),
        color   => [ 0, int rand(200) + 55, int rand(200) + 55 ],
        density => 1.0,
    );
    push @balls, $b;
}

my $joint = make_mouse_joint(
    target   => $ball->{body}->GetWorldCenter(),
    bodyA    => $ground->{body},
    bodyB    => $ball->{body},
    maxForce => 1000.0 * $ball->{body}->GetMass(),
);

my $app = SDLx::App->new(
    width  => $width,
    height => $height,
    dt     => $timestep,
    min_t  => $timestep / 2,
    flags  => SDL_DOUBLEBUF | SDL_HWSURFACE,
    eoq    => 1,
);

$app->add_event_handler(
    sub {
        my ($event) = @_;
        return unless $event->type == SDL_MOUSEMOTION;
        my ( undef, $x, $y ) = @{ SDL::Events::get_mouse_state() };
        $joint->SetTarget( make_vec2( s2w($x), s2w($y) ) );
    }
);

$app->add_show_handler(
    sub {
        $world->Step( $timestep, $vIters, $pIters );
        $world->ClearForces();

        # clear surface
        $app->draw_rect( undef, 0x000000FF );

        draw_circle($_) foreach @balls;
        draw_circle($ball);

        $app->update();
    }
);

$app->run();

# screen to world
sub s2w { return $_[0] * $mpp }

# world to screen
sub w2s { return $_[0] * $ppm }

sub make_vec2 {
    my ( $x, $y ) = @_;
    return Box2D::b2Vec2->new( $x, $y );
}

sub make_ground {
    my $body = make_body(
        x => $width / 2.0,
        y => $height,
    );
    my $edge
        = make_edge( [ 0.0, s2w($height) ], [ s2w($width), s2w($height) ] );
    my $fixture = make_fixture(
        shape => $edge,
        body  => $body,
    );
    return {
        shape   => $edge,
        body    => $body,
        fixture => $fixture,
    };
}

sub make_walls {
    my (@edges) = @_;
    my $body = make_body();
    my @fixtures;
    my @shapes;
    foreach (@edges) {
        my $edge    = make_edge(@$_);
        my $fixture = make_fixture(
            shape => $edge,
            body  => $body,
        );
        push @shapes,   $edge;
        push @fixtures, $fixture;
    }
    return {
        shapes   => \@shapes,
        body     => $body,
        fixtures => \@fixtures,
    };
}

sub make_ball {
    my (%options) = @_;
    my ( $x, $y, $r, $d ) = @options{qw( x y radius density )};

    my $body = make_body(
        x    => $x,
        y    => $y,
        type => 'dynamic',
    );

    my $circle = make_circle( radius => $r );

    my $fixture = make_fixture(
        shape   => $circle,
        body    => $body,
        density => $d,
    );

    return {
        body   => $body,
        shape  => $circle,
        fiture => $fixture,
        color  => $options{color},
    };
}

# shapes
sub make_edge {
    my ( $p1, $p2 ) = @_;
    my $edge = Box2D::b2PolygonShape->new();
    $edge->SetAsEdge( make_vec2(@$p1), make_vec2(@$p2) );
    return $edge;
}

sub make_circle {
    my (%options) = @_;
    my $circle = Box2D::b2CircleShape->new();
    $circle->m_radius( $options{radius} );
    return $circle;
}

# bodies
sub make_body {
    my (%options) = @_;
    my $bodyDef = Box2D::b2BodyDef->new();
    if ( exists $options{x} && exists $options{y} ) {
        my ( $x, $y ) = @options{qw( x y )};
        $bodyDef->position->Set( $x, $y );
    }
    if ( exists $options{type} ) {
        $bodyDef->type(Box2D::b2_dynamicBody) if $options{type} eq 'dynamic';
    }
    return $world->CreateBody($bodyDef);
}

# fixtures
sub make_fixture {
    my (%options) = @_;
    my ( $shape, $body, $density ) = @options{qw( shape body density )};
    my $fixtureDef = Box2D::b2FixtureDef->new();
    $fixtureDef->shape($shape);
    $fixtureDef->density($density);
    return $body->CreateFixtureDef($fixtureDef);
}

# joints
sub make_mouse_joint {
    my (%options) = @_;
    my $jointDef = Box2D::b2MouseJointDef->new();
    $jointDef->bodyA( $options{bodyA} )       if exists $options{bodyA};
    $jointDef->bodyB( $options{bodyB} )       if exists $options{bodyB};
    $jointDef->target( $options{target} )     if exists $options{target};
    $jointDef->maxForce( $options{maxForce} ) if exists $options{maxForce};
    $jointDef->frequencyHz( $options{frequencyHz} )
        if exists $options{frequencyHz};
    $jointDef->dampingRatio( $options{dampingRatio} )
        if exists $options{dampingRatio};
    my $joint = $world->CreateJoint($jointDef);
    return bless $joint, 'Box2D::b2MouseJoint';
}

# drawing
sub draw_circle {
    my ($circle) = @_;
    my ( $body, $shape, $c ) = @$circle{qw( body shape color )};
    my $p = $body->GetPosition();
    my ( $x, $y ) = ( $p->x, $p->y );
    my $r = $shape->m_radius;
    $app->draw_circle_filled( [ w2s($x), w2s($y) ], w2s($r), $c );
}