package OpenGL::Sandbox::Program;
use Moo;
use Carp;
use Try::Tiny;
use Log::Any '$log';
use OpenGL::Sandbox::MMap;
use OpenGL::Sandbox qw(
	warn_gl_errors
	glCreateProgram glDeleteProgram glAttachShader glDetachShader glLinkProgram glUseProgram 
	get_program_uniforms glGetAttribLocation_c
	GL_LINK_STATUS GL_FALSE GL_TRUE GL_CURRENT_PROGRAM GL_ACTIVE_UNIFORMS
);
BEGIN {
	try {
		OpenGL::Sandbox->import(qw( glGetIntegerv_p glGetProgramInfoLog_p glGetProgramiv_p ));
	}
	catch {
		try {
			require OpenGL::Modern::Helpers;
			OpenGL::Modern::Helpers->import(qw( glGetIntegerv_p glGetProgramInfoLog_p glGetProgramiv_p ));
		}
		catch {
			croak "Your OpenGL does not support version-4 shaders: ".$_;;
		};
	};
}

# ABSTRACT: Wrapper object for OpenGL shader program pipeline
our $VERSION = '0.120'; # VERSION


has name       => ( is => 'rw' );
has id         => ( is => 'lazy', predicate => 1 );
has shaders    => ( is => 'rw', default => sub { +{} } );
sub shader_list { values %{ shift->shaders } }

sub _build_id {
	my $self= shift;
	warn_gl_errors;
	my $id= glCreateProgram();
	$id && !warn_gl_errors or croak "glCreateProgram failed";
	my $log= glGetProgramInfoLog_p($id);
	warn "Shader Program ".$self->name.": ".$log
		if $log;
	$id;
}

has prepared   => ( is => 'rw' );
has uniforms   => ( is => 'lazy', predicate => 1, clearer => 1 );

sub _build_uniforms {
	get_program_uniforms(shift->id);
}

has _attribute_cache => ( is => 'rw', default => sub { +{} } );


sub bind {
	my $self= shift;
	$self->prepare unless $self->prepared;
	glUseProgram($self->id);
	return $self;
}


sub prepare {
	my $self= shift;
	return if $self->prepared;
	my $id= $self->id;
	warn_gl_errors;
	for ($self->shader_list) {
		$_->prepare;
		$log->debug("Attach shader $_") if $log->is_debug;
		glAttachShader($id, $_->id);
		!warn_gl_errors
			or croak "glAttachShader failed: ".glGetProgramInfoLog_p($id);
	}
	$log->debug("Link program ".$self->name) if $log->is_debug;
    glLinkProgram($id);
	!warn_gl_errors and glGetProgramiv_p($id, GL_LINK_STATUS) == GL_TRUE
		or croak "glLinkProgram failed: ".glGetProgramInfoLog_p($id);
	$self->prepared(1);
	return $self;
}

sub unprepare {
	my $self= shift;
	return unless $self->has_id && $self->prepared;
	glUseProgram(0) if glGetIntegerv_p(GL_CURRENT_PROGRAM, 1) == $self->id;
	$_->has_id && glDetachShader($self->id, $_->id) for $self->shader_list;
	$self->clear_uniforms;
	$self->prepared(0);
	return $self;
}


sub attr_by_name {
	my ($self, $name)= @_;
	$self->_attribute_cache->{$name} //= do {
		my $loc= glGetAttribLocation_c($self->id, $name);
		$loc >= 0? $loc : undef;
	};
}

sub uniform_location {
	my ($self, $name)= @_;
	($self->uniforms->{$name} // [])->[1];
}

sub set_uniform {
	my $self= shift;
	OpenGL::Sandbox::set_uniform($self->id, $self->uniforms, @_);
	$self;
}
*set= *set_uniform;

sub DESTROY {
	my $self= shift;
	if ($self->has_id) {
		$self->unprepare;
		glDeleteProgram(delete $self->{id});
	}
}

1;

__END__

=pod

=encoding UTF-8

=head1 NAME

OpenGL::Sandbox::Program - Wrapper object for OpenGL shader program pipeline

=head1 VERSION

version 0.120

=head1 DESCRIPTION

OpenGL shaders get combined into a pipeline.  In older versions of OpenGL, there was only one
program composed of a vertex shader and fragment shader, and attaching one of those shaders was
a global change.  In newer OpenGL, you may assemble multiple program pipelines and switch
between them.

This class tries to support both APIs, by holding a set of shaders which you can then "bind".
On newer OpenGL, this calls C<glUseProgram>.  On older OpenGL, this changes the global vertex
and fragment shaders to the ones referenced by this object.

=head1 ATTRIBUTES

=head2 name

Human-readable name of this program (not GL's integer "name")

=head2 prepared

Boolean; whether the program is ready to run.  This is always 'true' for older global-program
OpenGL.

=head2 shaders

A hashref of shaders, each of which will be attached to the program when it is activated.
The keys of the hashref are up to you, and simply to help diagnostics or merging shader
configurations together with defaults.

=head2 shader_list

A convenient accessor for listing out the values of the L</shader> hash.

=head2 id

The OpenGL integer 'name' of this program.  On older OpenGL with the global program, this will
always be C<undef>.  On newer OpenGL, this should always return a value because accessing it
will call C<glCreateProgram>.

=over

=item has_id

True if the id attribute has been lazy-loaded already.

=back

=head2 uniforms

Lazy-built hashref listing all uniforms of the compiled program.

=over

=item has_uniforms

Whether this has been lazy-built yet

=item clear_uniforms

Remove the cache, to be rebuilt on next use

=back

=head1 METHODS

=head2 bind

  $program->bind;

Begin using this program as the active GL pipeline.

Returns C<$self> for convenient chaining.

=head2 prepare

For relevant implementations, this attaches the shaders and links the program.
If it fails, this throws an exception.  For OpenGL 4 implementation, this only happens
once, and any changes to L</shaders> afterward are ignored.  Use L</unprepare> to remove
the compiled state and be able to rearrange the shaders.

Returns C<$self> for convenient chaining.

=head2 unprepare

Release resources allocated by L</prepare>.

=head2 attr_by_name

Return the attribute ID of the given name, for the prepared program.

=head2 uniform_location

Return the uniform ID of the given name, for the prepared program.

=head2 set_uniform

  $prog->set_uniform( $name, \@values );
  $prog->set_uniform( $name, $opengl_array );

Set the value of a uniform.  This attempts to guess at the size/geometry of the uniform based
on the number or type of values given.

=head2 set

Alias for C<set_uniform>.

=head1 AUTHOR

Michael Conrad <mike@nrdvana.net>

=head1 COPYRIGHT AND LICENSE

This software is copyright (c) 2019 by Michael Conrad.

This is free software; you can redistribute it and/or modify it under
the same terms as the Perl 5 programming language system itself.

=cut
