package Program;

BEGIN {
	# export these
	*{main::NEW} = \&Program::NEW;
	*{main::DEL} = \&Program::DEL;
	*{main::FLT} = \&Program::FLT;
	*{main::SET} = \&Program::SET;

	*{main::FLTIN} = \&Program::FLTIN;
	*{main::FLTOUT} = \&Program::FLTOUT;
	*{main::FLTINOUT} = \&Program::FLTINOUT;
	
	*{main::NEWPOST} = \&Program::NEWPOST;

}

sub NEW() { return 1; }
sub DEL() { return 2; }
sub FLT() { return 3; }
sub SET() { return 4; }
sub NEWPOST() { return 5; }

sub FLTIN() { return 1; }
sub FLTOUT() { return 2; }
sub FLTINOUT() { return 3; }

sub fltinout($) {
	my ($a) = @_;
	return -cos($a*3.14159265)/2+0.5;
}

sub fltin($) {
	my ($a) = @_;
	return -cos($a*3.14159265/2)+1;
}

sub fltout($) {
	my ($a) = @_;
	return sin($a*3.14159265/2);
}

sub logstr($) {
	my ($str) = @_;

}

sub is_numeric {
	return defined eval { $_[ 0] == 0 };
}

sub new($$) {
	my ($class) = @_;


	my $self = {};

	$self->{"curevent"} = 0;
	$self->{"layers"} = [];
	$self->{"pbuffers"} = ${main::pbuffers};
	$self->{"pbuffernum"} = 0;
	
	bless $self;

	return $self;
}

sub Preload($$) {
    my ($self, $params) = @_;

	if (defined($self->{"preloaded"}) && $self->{"preloaded"} == 1) {
		return;
	}
	print "Preload ..\n";
    if (scalar(keys %{$params}) != 1 || !defined($params->{"program"})) {
        return;
    }

    foreach $event (@{$params->{"program"}}) {
        if ($event->[1] != NEW && $event->[1] != NEWPOST) {
            next;
        }

        logstr( "create: ".$event->[2]." = $event->[4]\n" );
        my $engine = &{$event->[2] . "::new"} ($event->[2]); # constructor
        $engine->SetParams($event->[5]); # first step to activate most engines
        $self->AddObject($engine, $event->[4]);
    }
    $self->{"preloaded"} = 1;
    
    print "done.\n";
}

sub SetParams($$) {
	my ($self,$params) = @_;
	my $key;

	foreach $key (keys %{$params}) {
		if($key eq "program") {
			# Load program
			$self->LoadProgram($params->{$key});
			$self->Preload($params);
		} else {
			$self->{"params"}->{$key} = $params->{$key};
		}
	}
}

sub Render($$) {
	my ($self, $msec) = @_;
	my $curevent = $self->{"curevent"};
	my $fb = OpenGL::getCurrentFB();

	# logstr( "Render frame @ $msec msec\n" );

	# Scan through the event list to see if anything should be started or ended

	if($curevent >= @{$self->{"program"}}) {
		return 1;
	}

	while(defined($self->{"program"}->[$curevent]) && $self->{"program"}->[$curevent]->[0] <= $msec) {
		my $event = $self->{"program"}->[$curevent];

		if($event->[1] == NEW || $event->[1] == NEWPOST) {

			logstr( "Event @ " . $event->[0] . " msec: NEW " . $event->[3] . " object\n" );

			# Call the package's constructor
			my $engine = $self->FindObject($event->[4])->{"engine"}; # find engine from preload

			$engine->SetParams($event->[5]);

			$self->RemoveLayer($event->[4]);
			$self->AddLayer($engine, $event->[4], $event->[3], $event->[5], $event->[0], $event->[1] == NEWPOST);

			# Find next float point and add it if necessary

			my $nextevent = $self->FindNextFlt($curevent, $event->[4]);

			if(defined($nextevent)) {
				logstr( "Found FLT for object at event $nextevent\n" );
				$self->FloatLayer($event->[4], $nextevent);
			} else {
				logstr( "No FLT after event $event->[0] msec, freezing $event->[4] object\n" );
				$self->FloatLayer($event->[4], 0);
			}
		}
		elsif($self->{"program"}->[$curevent]->[1] == DEL) {
			logstr( "Event @ $event->[0] msec, DEL $event->[2] object\n" );

			$self->RemoveLayer($event->[2]);

		}
		elsif($self->{"program"}->[$curevent]->[1] == FLT) {
			# Passed a float point, find the next and set it
			
			# Set layer to end values
			$self->SetLayer($event->[2], $event->[3], $event->[0]);

			logstr( "Event @ $event->[0] msec, FLT $event->[2] object\n" );

			my $nextevent = $self->FindNextFlt($curevent, $event->[2]);

			if(defined($nextevent)) {
				logstr( "Found FLT for object at event $nextevent\n" );
				$self->FloatLayer($event->[2], $nextevent);
			} else {
				logstr( "No FLT after event $event->[0] msec, freezing $event->[2] object\n" );
				$self->FloatLayer($event->[2], 0);
			}

		}
		elsif($self->{"program"}->[$curevent]->[1] == SET) {
			# Found a SET point (har har)

			logstr( "Event @ $event->[0] msec, SET $event->[2] object\n" );

			$self->SetLayer($event->[2], $event->[3], $event->[0]);

			# Find next float point and add it if necessary

			my $nextevent = $self->FindNextFlt($curevent, $event->[2]);

			if(defined($nextevent)) {
				logstr( "Found FLT for object at event $nextevent\n" );
				$self->FloatLayer($event->[2], $nextevent);
			} else {
				logstr( "No FLT after event $event->[0] msec, freezing $event->[2] object\n" );
				$self->FloatLayer($event->[2], 0);
			}
		}

		$curevent++;
	}

	# Scan through the current effects to see if they should be floated to a next value
	foreach $layer (@{$self->{"layers"}}) {
		if(defined($layer->{"fltevent"})) {
			# This layer has a 'float' param, so it should be smoothly moved to the following pos
			my $msecstart = $layer->{"laststart"};
			my $msecend = $self->{"program"}->[$layer->{"fltevent"}]->[0];
			my $paramstart = $layer->{"params"};
			my $paramend = $self->{"program"}->[$layer->{"fltevent"}]->[3];
			my $type = $self->{"program"}->[$layer->{"fltevent"}]->[4];

			my $params = FloatParams(($msec-$msecstart) / ($msecend-$msecstart), $paramstart, $paramend, $type);

			$layer->{"engine"}->SetParams($params);
		}
	}

	@sorted = sort { $a->{"depth"} <=> $b->{"depth"} } @{$self->{"layers"}};
	
	@post = grep { $_->{"ispost"} } @sorted;

	if(@post > 0) {
		# Post-processing is active, render to a pbuffer
		$self->{"pbuffers"}->[$self->{"pbuffernum"}]->renderTo();
		OpenGL::glClear();
	}
	
	# logstr( "Renderloop: (" . @{$self->{"layers"}} . " layers):\n" );
	# Render all the layers in ascending order
	foreach $layer (@sorted) {

		my $reltime = $msec - $layer->{"start"};

		if(!$layer->{"ispost"}) {
			$layer->{"engine"}->Render($reltime);
		}
		
		elsif($post[0]->{"depth"} >= $layer->{"depth"}) {
			$thispost = shift(@post);
			
			$self->{"pbuffers"}->[$self->{"pbuffernum"}]->renderFrom();
			$self->{"pbuffers"}->[$self->{"pbuffernum"}]->generateMipmaps();
			
			if(@post == 0) {
				# No more post-processors, render to framebuffer we were called with
				OpenGL::renderTo($fb);
			} else {
				# More post-processing later, render to texture
				$self->{"pbuffernum"} = 1 - $self->{"pbuffernum"};
				$self->{"pbuffers"}->[$self->{"pbuffernum"}]->renderTo();
			}
			
			# Render the postprocessing layer
			
			$thispost->{"engine"}->Render($msec);
			
			# Next layers are automatically also rendered to this framebuffer, because it is now active
		}
	}
	# logstr( "\n" );

	$self->{"curevent"} = $curevent;

	return 0;
}

sub AddObject($$$$$) {
    my ($self, $engine, $name) = @_;

    my $newobject;
    $newobject->{"engine"} = $engine;
    $newobject->{"name"} = $name;

    push @{$self->{"objects"}}, $newobject;
}
sub FindObject($$) {
    my ($self, $name) = @_;

    foreach $obj (@{$self->{"objects"}}) {
        if($obj->{"name"} eq $name) {
            return $obj;
        }
    }
    return false;
}
sub RemoveObject($$) {
    my ($self, $name) = @_;

    # remove only first occurrence, this way because of double names
    my (@copy, $found) = ((), 0);

    foreach $obj (@{$self->{"objects"}}) {
        if(!$found && $obj->{"name"} eq $name) {
            $found = 1;
        } else {
            push @copy, $obj;
        }
    }
    @{$self->{"objects"}} = @copy;
}

sub AddLayer($$$$$$) {
	my ($self, $engine, $name, $depth, $params, $start, $ispost) = @_;

	my $newlayer;
	my %params = %{$params};

	$newlayer->{"engine"} = $engine;
	$newlayer->{"name"} = $name;
	$newlayer->{"depth"} = $depth;
	$newlayer->{"params"} = \%params;
	$newlayer->{"start"} = $start;
	$newlayer->{"laststart"} = $start;
	$newlayer->{"ispost"} = $ispost;
		
	push @{$self->{"layers"}}, $newlayer;
}

sub RemoveLayer($$) {
	my ($self, $name) = @_;

	my $layer;
	
	@{$self->{"layers"}} = grep { $_->{"name"} ne $name } @{$self->{"layers"}};
}

sub SetLayer($$$$) {
	my ($self, $name, $params, $start) = @_;

	# Find the correct layer
	foreach $layer (@{$self->{"layers"}}) {
		if($layer->{"name"} eq $name) {
			$layer->{"params"} = FloatParams(1,$layer->{"params"},$params);
			$layer->{"curevent"} = $event;
			$layer->{"laststart"} = $start;
			$layer->{"engine"}->SetParams($layer->{"params"});
		}
	}
}
sub FloatLayer($$$) {
	my ($self, $name, $fltevent) = @_;

	my $layer;

	# Find the correct layer
	foreach $layer (@{$self->{"layers"}}) {
		if($layer->{"name"} eq $name) {
			if($fltevent == 0) {
				# turn off float
				undef($layer->{"fltevent"});
			} else {
				$layer->{"fltevent"} = $fltevent;
			}
		}
	}
}

sub LoadProgram($$) {
	my ($self, $program) = @_;

	logstr( "Loading program, (" . (0 + @{$program}) . " events)\n" );
	# Copy program
	my @program= @{$program};

	# Sort by start moment
	@program = sort { $a->[0] <=> $b->[0] } @program;


	$self->{"program"} = \@program;
}

sub FindNextFlt($$$$) {
	my ($self, $first, $name) = @_;

	$first++;
	while($first < @{$self->{"program"}}) {
		if(	$self->{"program"}->[$first]->[1] == FLT &&
			$self->{"program"}->[$first]->[2] eq $name ) {
				return $first;
		}
				
		if(	$self->{"program"}->[$first]->[1] == SET &&
			$self->{"program"}->[$first]->[2] eq $name ) {
				return;
		}

		if(	$self->{"program"}->[$first]->[1] == DEL &&
			$self->{"program"}->[$first]->[2] eq $name ) {
				return;
		}

		if(	$self->{"program"}->[$first]->[1] == NEW &&
			$self->{"program"}->[$first]->[4] eq $name ) {
				return;
		}
		$first++;
	}

	return;
}

sub FloatParams($$$$) {
	my ($n, $p1, $p2, $type) = @_;
	my $key;
	my %output = %{$p1};

	my @sharedkeys;
	
	if(!defined($type)) {
		$n = $n;
	} elsif($type == FLTIN) {
		$n = fltin($n);
	} elsif($type == FLTOUT) {
		$n = fltout($n);
	} elsif($type == FLTINOUT) {
		$n = fltinout($n);
	}

	@sharedkeys = grep { $a = $_; grep { $_ eq $a } keys %{$p2} } keys %{$p1};

	foreach $key (@sharedkeys) {
		if(is_numeric($p1->{$key}) && is_numeric($p2->{$key})) {
			$output{$key} = $n * ($p2->{$key} - $p1->{$key}) + $p1->{$key};
		}
	}

	return \%output;
}

1;
