function [minv, maxv, meanv] = moglComputeMinMaxMeanOfTexture(texid, pingpongfbo1, pingpongfbo2, fastpath)
% [minv, maxv, meanv] = moglComputeMinMaxMeanOfTexture(texid, pingpongfbo1, pingpongfbo2, fastpath)
%
% This function expects a square RGB texture as input, whose color channels
% contain identical values L=R=G=B where L is a grayscale (luminance) input
% image. It computes and returns its global minimum, maximum and mean luminance
% on the GPU.
%
% The width and height of the texture need to be divisible by two (even numbers).
% It only works on RECTANGLE textures, not on standard power-of-two textures!
% You are not allowed to use an input texture 'texid' that is identical to
% the color buffer texture of the FBO 'pingpongfbo2'!

% Notes:
% FBO ping-ponging takes 13.7 ms on GeForce-7800 for 256 by 256 image.
%
% History:
% 30.5.2006 Written (MK).

% Need handle to GL constant definitions:
global GL;

% We cache our reduction shader for reuse on later invocations:
persistent reduceshader;
if isempty(reduceshader)
    reduceshader = LoadGLSLProgramFromFiles('MinMaxMeanReduceShader');
end;

if nargin < 4
    fastpath = 0;
else
    if isempty(fastpath)
        fastpath = 0;
    end;
end;

if nargin < 2
    error('Need texid of input texture and the fbo-handles of at least the first ping-pong FBO!');
end;

% Query size of input texture:
glBindTexture(GL.TEXTURE_RECTANGLE_EXT, texid);
w = glGetTexLevelParameteriv(GL.TEXTURE_RECTANGLE_EXT, 0, GL.TEXTURE_WIDTH);
h = glGetTexLevelParameteriv(GL.TEXTURE_RECTANGLE_EXT, 0, GL.TEXTURE_HEIGHT);

% We skip all error checking and such if fastpath > 0. This saves about 4
% ms of setup time.
if fastpath == 0
    % Take the slow, but safe route:    
    if ~glIsFramebufferEXT(pingpongfbo1)
        error('Invalid pingpongfbo1 identifier passed. This is not a valid FBO.');
    end;

    if nargin < 3
        pingpongfbo2 = 0;
    end;

    if isempty(pingpongfbo2)
        pingpongfbo2 = 0;
    end;

    if pingpongfbo2 > 0
        % Second bounce buffer provided. Check it.
        if ~glIsFramebufferEXT(pingpongfbo2)
            error('Invalid pingpongfbo2 identifier passed. This is not a valid FBO.');
        end;
        singlefbo = 0;
    else
        % Only one FBO provided, make sure that one has two color attachments:
        glBindFramebufferEXT(GL.FRAMEBUFFER_EXT, pingpongfbo1);
        if glGetFramebufferAttachmentParameterivEXT(GL.FRAMEBUFFER_EXT, GL.COLOR_ATTACHMENT1_EXT, GL.FRAMEBUFFER_ATTACHMENT_OBJECT_TYPE_EXT)~=GL.TEXTURE
            error('Only pingpongfbo1 provided, not pingpongfbo2 *and* pingpongfbo1 does not have 2 color buffers. This will not work...');
        end;
        singlefbo = 1;
    end;

    if w~=h || w<2 || mod(w,2)>0
        error('Invalid size of input texture: Needs to be even, width = height and at least 2 pixels wide.');
    end;

    % Query size of FBO's:
    glBindFramebufferEXT(GL.FRAMEBUFFER_EXT, pingpongfbo1);
    fbotexid = glGetFramebufferAttachmentParameterivEXT(GL.FRAMEBUFFER_EXT, GL.COLOR_ATTACHMENT0_EXT, GL.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME_EXT);
    glBindTexture(GL.TEXTURE_RECTANGLE_EXT, fbotexid);
    fw = glGetTexLevelParameteriv(GL.TEXTURE_RECTANGLE_EXT, 0, GL.TEXTURE_WIDTH);
    fh = glGetTexLevelParameteriv(GL.TEXTURE_RECTANGLE_EXT, 0, GL.TEXTURE_HEIGHT);
    glBindTexture(GL.TEXTURE_RECTANGLE_EXT, 0);
    if fw < w/2 || fh < h/2
        error('Pingpong FBO 1 is too small for reduce operation!');
    end;

    % Query size of FBO's:
    if singlefbo == 0
        glBindFramebufferEXT(GL.FRAMEBUFFER_EXT, pingpongfbo2);
        fbotexid = glGetFramebufferAttachmentParameterivEXT(GL.FRAMEBUFFER_EXT, GL.COLOR_ATTACHMENT0_EXT, GL.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME_EXT);
        glBindTexture(GL.TEXTURE_RECTANGLE_EXT, fbotexid);
        fw = glGetTexLevelParameteriv(GL.TEXTURE_RECTANGLE_EXT, 0, GL.TEXTURE_WIDTH);
        fh = glGetTexLevelParameteriv(GL.TEXTURE_RECTANGLE_EXT, 0, GL.TEXTURE_HEIGHT);
        glBindTexture(GL.TEXTURE_RECTANGLE_EXT, 0);
        if fw < w/2 || fh < h/2
            error('Pingpong FBO 2 is too small for reduce operation!');
        end;
    end;

    % Make sure we don't try to read from the pingpongfbo2's texture as input
    % texture - that would cause undefined behaviour.
    if singlefbo == 0
        if texid == moglGetTexForFBO(pingpongfbo2)
            error('Input texture texid is the texture attached to pingpongfbo2! Thats forbidden! Aborted.');
        end;
    else
        if texid == moglGetTexForFBO(pingpongfbo1, 2)
            error('Input texture texid is the texture attached to color buffer 2 of pingpongfbo1! That is forbidden! Aborted.');
        end;
    end;
else
    % Fast path: Minimal setup, no error checks:
    if pingpongfbo2 > 0
        singlefbo = 0;
    else
        singlefbo = 1;
    end;
end;

% Ok we survived error-checking, everything is fine. Do it:
reducestep = 2;
numiters = ceil(log(double(w))/log(reducestep));
currentsize = w;

% On first iteration we operate on the input texture:
inputtex = texid;

% Setup texture mapping:
glDisable(GL.TEXTURE_2D);
glEnable(GL.TEXTURE_RECTANGLE_EXT);

% Make sure we use nearest neighbour sampling:
glTexParameteri(GL.TEXTURE_RECTANGLE_EXT, GL.TEXTURE_MIN_FILTER, GL.NEAREST);
glTexParameteri(GL.TEXTURE_RECTANGLE_EXT, GL.TEXTURE_MAG_FILTER, GL.NEAREST);
% And that we clamp to edge:
glTexParameteri(GL.TEXTURE_RECTANGLE_EXT, GL.TEXTURE_WRAP_S, GL.CLAMP);
glTexParameteri(GL.TEXTURE_RECTANGLE_EXT, GL.TEXTURE_WRAP_T, GL.CLAMP);

% Do a full setup of the initial FBO and colorbuffer before iterating,
% because this is very costly. The iteration loops only do the minimal
% amount of switching via glXXX calls to reduce switch-overhead:
if singlefbo==1
    % One FBO, two buffers:
    moglChooseFBO(pingpongfbo1, 2);
    buffertex(1)=glGetFramebufferAttachmentParameterivEXT(GL.FRAMEBUFFER_EXT, GL.COLOR_ATTACHMENT0_EXT, GL.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME_EXT);
    buffertex(2)=glGetFramebufferAttachmentParameterivEXT(GL.FRAMEBUFFER_EXT, GL.COLOR_ATTACHMENT1_EXT, GL.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME_EXT);
else
    % Two FBO's:
    glBindFramebufferEXT(GL.FRAMEBUFFER_EXT, pingpongfbo1);
    buffertex(1)=glGetFramebufferAttachmentParameterivEXT(GL.FRAMEBUFFER_EXT, GL.COLOR_ATTACHMENT0_EXT, GL.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME_EXT);    
    moglChooseFBO(pingpongfbo2, 1);
    buffertex(2)=glGetFramebufferAttachmentParameterivEXT(GL.FRAMEBUFFER_EXT, GL.COLOR_ATTACHMENT0_EXT, GL.FRAMEBUFFER_ATTACHMENT_OBJECT_NAME_EXT);    
end;

% Activate reduce-shader:
glUseProgram(reduceshader);

% Do numiters pingpong-reduce operations:
bufferid = 0;
for i=1:numiters
    % Compute drawbuffers id and its texturehandle:
    bufferid = 1 - bufferid;
    futuretex = buffertex(bufferid + 1);

    % Bind proper FBO as target for intermediate reduce results:
    if singlefbo == 0
        % Use two FBO's for pingpong:
        if bufferid==0
            glBindFramebufferEXT(GL.FRAMEBUFFER_EXT, pingpongfbo1);
        else
            glBindFramebufferEXT(GL.FRAMEBUFFER_EXT, pingpongfbo2);
        end;
    else
        % Use one dual-buffer FBO for pingpong:
        % We only switch the draw-buffer to keep the overhead as low as
        % possible:
        glDrawBuffer(GL.COLOR_ATTACHMENT0_EXT + bufferid);
    end;
    
    % Perform reduction blit operation to a size of 'currentsize' by 'currentsize':
    currentsize = floor(currentsize / reducestep);

    % Do it: We use fast-blit mode (1) because we did texture setup
    % ourselves:
    moglBlitTexture(inputtex, 0, 0, currentsize, currentsize, 1);

    % Assign source texture for next pass:
    inputtex = futuretex;
end;

% Ok, pixel (0,0) of our currently bound FBO should contain the final
% result. Read it out:
if singlefbo == 0, bufferid = 0; end;
glReadBuffer(GL.COLOR_ATTACHMENT0_EXT + bufferid);
resultpixel = double(glReadPixels(0, 0, 1, 1, GL.RGB, GL.FLOAT));
meanv = resultpixel(1);
maxv  = resultpixel(2);
minv  = resultpixel(3);

% Disable texture mapping...
glDisable(GL.TEXTURE_RECTANGLE_EXT);
glBindTexture(GL.TEXTURE_RECTANGLE_EXT, 0);
% ...disable reduce - shader:
glUseProgram(0);

% Well done!
return;