with
     openGL.Tasks,

     GL.Pointers,
     GL.lean,

     ada.Characters.latin_1,
     interfaces.C.Strings;


package body openGL.Program
is
   use gl.lean,
       Interfaces;

   compiling_in_debug_Mode : constant Boolean := True;

   type Shader_view is access all Shader.item'Class;


   --------------
   --  Parameters
   --

   procedure Program_is (Self : in out Parameters;   Now : in openGL.Program.view)
   is
   begin
      Self.Program := Now;
   end Program_is;


   function Program (Self : in Parameters) return openGL.Program.view
   is
   begin
      return Self.Program;
   end Program;


   ---------
   --- Forge
   --

   procedure define (Self : in out Item;   use_vertex_Shader   : in Shader.view;
                                           use_fragment_Shader : in Shader.view)
   is
   begin
      Tasks.check;

      Self.gl_Program := glCreateProgram;

      glAttachShader (Self.gl_Program,    use_vertex_Shader.gl_Shader);
      glAttachShader (Self.gl_Program,  use_fragment_Shader.gl_Shader);

      Self.  vertex_Shader :=   use_vertex_Shader;
      Self.fragment_Shader := use_fragment_Shader;

      glLinkProgram (Self.gl_Program);

      declare
         use type C.int;
         Status : aliased gl.glInt;
      begin
         glGetProgramiv (Self.gl_Program,
                         GL_LINK_STATUS,
                         Status'unchecked_Access);

         if Status = 0
         then
            declare
               link_Log : constant String := Self.ProgramInfoLog;
            begin
               Self.destroy;
               raise Error with "Program link error ~ " & link_Log;
            end;
         end if;
      end;

      if compiling_in_debug_Mode
      then
         glValidateProgram (Self.gl_Program);
      end if;
   end define;



   procedure define (Self : in out Item;   use_vertex_Shader_File   : in String;
                                           use_fragment_Shader_File : in String)
   is
      use openGL.Shader;
      the_vertex_Shader   : constant Shader_view := new openGL.Shader.item;
      the_fragment_Shader : constant Shader_view := new openGL.Shader.item;
   begin
      the_vertex_Shader  .define (openGL.Shader.vertex,     use_vertex_Shader_File);
      the_fragment_Shader.define (openGL.Shader.fragment, use_fragment_Shader_File);

      Self.define (  the_vertex_Shader.all'Access,
                   the_fragment_Shader.all'Access);
   end define;



   procedure destroy (Self : in out Item)
   is
   begin
      Tasks.check;
      glDeleteProgram (Self.gl_Program);
   end destroy;


   --------------
   --  Attributes
   --

   function Attribute (Self : access Item'Class;   Named : in String) return openGL.Attribute.view
   is
   begin
      for Each in 1 .. Self.attribute_Count
      loop
         if Self.Attributes (Each).Name = Named
         then
            return Self.Attributes (Each);
         end if;
      end loop;

      raise Error with "'" & Named & "' is not a valid program attribute.";
   end Attribute;



   function attribute_Location (Self : access Item'Class;   Named : in String) return gl.GLuint
   is
      use      gl.Pointers;
      use type gl.GLint;

      attribute_Name : C.strings.chars_ptr := C.Strings.new_String (Named & ada.characters.Latin_1.NUL);

   begin
      Tasks.check;

      declare
         gl_Location : constant gl.GLint := glGetAttribLocation (Self.gl_Program,
                                                                 to_GLchar_access (attribute_Name));
      begin
         if gl_Location = -1
         then
            raise Error with "Requested attribute '" & Named & "' has no gl location in program.";
         end if;

         C.Strings.free (attribute_Name);

         return gl.GLuint (gl_Location);
      end;
   end attribute_Location;



   function is_defined (Self : in Item'Class) return Boolean
   is
      use type a_gl_Program;
   begin
      return Self.gl_Program /= 0;
   end is_defined;



   function ProgramInfoLog (Self : in Item) return String
   is
      use C, GL;

      info_log_Length : aliased glInt   := 0;
      chars_Written   : aliased glSizei := 0;

   begin
      Tasks.check;

      glGetProgramiv (Self.gl_Program,
                      GL_INFO_LOG_LENGTH,
                      info_log_Length'unchecked_Access);

      if info_log_Length = 0 then
         return "";
      end if;

      declare
         use GL.Pointers;
         info_Log     : aliased  C.char_array        := C.char_array' [1 .. C.size_t (info_log_Length) => <>];
         info_Log_ptr : constant C.strings.chars_ptr := C.strings.to_chars_ptr (info_Log'unchecked_Access);
      begin
         glGetProgramInfoLog (Self.gl_Program,
                              glSizei (info_log_Length),
                              chars_Written'unchecked_Access,
                              to_GLchar_access (info_Log_ptr));
         return C.to_Ada (info_Log);
      end;
   end ProgramInfoLog;



   function uniform_Variable (Self : access Item'Class;   Named : in String) return Variable.uniform.bool
   is
      the_Variable : Variable.uniform.bool;
   begin
      the_Variable.define (Self, Named);
      return the_Variable;
   end uniform_Variable;



   function uniform_Variable (Self : access Item'Class;   Named : in String) return Variable.uniform.int
   is
      the_Variable : Variable.uniform.int;
   begin
      the_Variable.define (Self, Named);
      return the_Variable;
   end uniform_Variable;



   function uniform_Variable (Self : access Item'Class;   Named : in String) return Variable.uniform.float
   is
      the_Variable : Variable.uniform.float;
   begin
      the_Variable.define (Self, Named);
      return the_Variable;
   end uniform_Variable;



   function uniform_Variable (Self : access Item'Class;   Named : in String) return Variable.uniform.vec3
   is
      the_Variable : Variable.uniform.vec3;
   begin
      the_Variable.define (Self, Named);
      return the_Variable;
   end uniform_Variable;



   function uniform_Variable (Self : access Item'Class;   Named : in String) return Variable.uniform.vec4
   is
      the_Variable : Variable.uniform.vec4;
   begin
      the_Variable.define (Self, Named);
      return the_Variable;
   end uniform_Variable;



   function uniform_Variable (Self : access Item'Class;   Named : in String) return Variable.uniform.mat3
   is
      the_Variable : Variable.uniform.mat3;
   begin
      the_Variable.define (Self, Named);
      return the_Variable;
   end uniform_Variable;



   function uniform_Variable (Self : access Item'Class;   Named : in String) return Variable.uniform.mat4
   is
      the_Variable : Variable.uniform.mat4;
   begin
      the_Variable.define (Self, Named);
      return the_Variable;
   end uniform_Variable;


   function uniform_Variable (Self : access Item'Class;   Named : in String) return Variable.uniform.sampler2D
   is
      the_Variable : Variable.uniform.sampler2D;
   begin
      the_Variable.define (Self, Named);
      return the_Variable;
   end uniform_Variable;


   --------------
   --  Operations
   --

   procedure add (Self : in out Item;   Attribute : in openGL.Attribute.view)
   is
   begin
      Self.attribute_Count                   := Self.attribute_Count + 1;
      Self.Attributes (Self.attribute_Count) := Attribute;
   end add;



   procedure enable (Self : in out Item)
   is
      use type gl.GLuint;
   begin
      Tasks.check;

      if Self.gl_Program = 0
      then
         Item'Class (Self).define;     -- TODO: This appears to do nothing.
      end if;

      glUseProgram (self.gl_Program);
   end enable;



   procedure enable_Attributes (Self : in Item)
   is
   begin
      for Each in 1 .. Self.attribute_Count
      loop
         Self.Attributes (Each).enable;
      end loop;
   end enable_Attributes;



   procedure mvp_Transform_is (Self : in out Item;   Now : in Matrix_4x4)
   is
   begin
      Self.mvp_Transform := Now;
   end mvp_Transform_is;



   procedure Scale_is (Self : in out Item;   Now : in Vector_3)
   is
   begin
      Self.Scale := Now;
   end Scale_is;



   procedure set_Uniforms (Self : in Item)
   is
      the_mvp_Uniform   : constant Variable.uniform.mat4 := Self.uniform_Variable ("mvp_Transform");
      the_scale_Uniform : constant Variable.uniform.vec3 := Self.uniform_Variable ("Scale");
   begin
      the_mvp_Uniform  .Value_is (Self.mvp_Transform);
      the_scale_Uniform.Value_is (Self.Scale);
   end set_Uniforms;



   --  Privvy
   --

   function gl_Program (Self : in Item) return a_gl_Program
   is
   begin
      return Self.gl_Program;
   end gl_Program;


end openGL.Program;
