%IMAGESC3D 3D wrapper for imagesc
% imagesc3D supports the same parameters as Matlab's imagesc. In addition, the following 
% parameters can be set
%
% init_frame...       starting frame number (default 1)
% slider_axis...      axis along which you want to use imagesc (default 3)
% fps...              frames per second (default 25); will be adjusted by a factor of 1.2 to account for internal overhead
% title_list...       individual title for each frame (default {})
% loop...             run in a loop (default false)
% reset_frame...      stop resets frame to init_frame (default false)
% autoplay...         stark movie automatically (default false)
% slider_position...  slider position [left bottom width height] (default center of axis)
% play_position...    play button position [left bottom width height]
% edit_position...    edit box position [left bottom width height]
% show_play_button... show/hide button; needs to be visible if loop=true; (default true)
% show_edit_box...    show/hide box
% fnct...             data processing function
% order...            change slice order in stack
% save_movie...       specify filename if a movie shall be written
% movie_quality...    image quality of the saved movie
%
% Complex images will be converted to RGB using c2image.
%
% If you are not using 'autplay', you can also set a global title instead
% of a title list (similar to imagesc) and use '%d' to get the slice number
%    title('Random block - slice %d')
% 
%
% EXAMPLES:
%       imagesc3D(rand(256, 256, 100), 'fps', 10, 'loop', true)
%       imagesc3D(rand(256, 256)*1j)
%       imagesc3D(rand(20, 256, 256), 'slider_axis', 1);
%
%
%    Additionally, you can use imagesc/imagesc3D routines and trigger the movie by
%    calling the play method of a specified axis:
%
%       figure(1); 
%       imagesc3D(rand(256, 256, 100), 'fps', 20);
%       title('Random block - slice %d');
%       colorbar();
%       ax = gca;
%       ax.play();
%


%*-----------------------------------------------------------------------*
%|                                                                       |
%|  Except where otherwise noted, this work is licensed under a          |
%|  Creative Commons Attribution-NonCommercial-ShareAlike 4.0            |
%|  International (CC BY-NC-SA 4.0) license.                             |
%|                                                                       |
%|  Copyright (c) 2017 by Paul Scherrer Institute (http://www.psi.ch)    |
%|                                                                       |
%|       Author: CXS group, PSI                                          |
%*-----------------------------------------------------------------------*
% You may use this code with the following provisions:
%
% If the code is fully or partially redistributed, or rewritten in another
%   computing language this notice should be included in the redistribution.
%
% If this code, or subfunctions or parts of it, is used for research in a 
%   publication or if it is fully or partially rewritten for another 
%   computing language the authors and institution should be acknowledged 
%   in written form in the publication: “Data processing was carried out 
%   using the “cSAXS software package” developed by the CXS group,
%   Paul Scherrer Institut, Switzerland.” 
%   Variations on the latter text can be incorporated upon discussion with 
%   the CXS group if needed to more specifically reflect the use of the package 
%   for the published work.
%
% A publication that focuses on describing features, or parameters, that
%    are already existing in the code should be first discussed with the
%    authors.
%   
% This code and subroutines are part of a continuous development, they 
%    are provided “as they are” without guarantees or liability on part
%    of PSI or the authors. It is the user responsibility to ensure its 
%    proper use and the correctness of the results.

function imagesc3D(varargin)
import math.isint
import plotting.c2image

ax_img = {};
if nargin == 1
    img = varargin{1};
    vararg = {};
elseif nargin == 3 && isnumeric(varargin{1}) && isnumeric(varargin{2})  && (islogical(varargin{3}) || isnumeric(varargin{3}))
    img = varargin{3};
    ax_img = varargin(1:2);
    vararg = {};
elseif (islogical(varargin{1}) || isnumeric(varargin{1})) && ischar(varargin{2})
    % assume that first argument is images, and next are
    % string+arguments
    img = varargin{1};
    vararg = varargin(2:end);
elseif isnumeric(varargin{1}) && isnumeric(varargin{2})  && (islogical(varargin{3}) || isnumeric(varargin{3})) 
    % assume that first two arguments are axis,and third is images, and next are
    % string+arguments
    ax_img = varargin(1:2);
    img = varargin{3};
    vararg = varargin(4:end);
else
   error('Unknown combination of parameters') 
end

ax = gca;
pos = ax.Position;
slider_default = [pos(1)+pos(3)/2-0.06 pos(2)-0.1 0.14 0.05];
play_default = [slider_default(1)-0.1 pos(2)-0.1 0.08 0.05];
edit_default = [slider_default(1)+slider_default(3)+0.01 slider_default(2) 0.08 0.05];

par = inputParser;
par.addParameter('fps', 25, @isnumeric)  % maximal frame rate 
par.addParameter('init_frame', 1, @isnumeric)  % starting frame number 
par.addParameter('title_list', {}, @iscell)  % list of titles for each frame
par.addParameter('slider_axis',3, @isnumeric)  % array axis
par.addParameter('loop', false, @islogical) % loop
par.addParameter('reset_frame', false, @islogical) % stop resets frame to init_frame
par.addParameter('autoplay', false, @islogical) % start loop automatically
par.addParameter('slider_position',slider_default, @isnumeric)  % slider position; [left bottom width height]
par.addParameter('play_position',play_default, @isnumeric)  % slider position; [left bottom width height]
par.addParameter('edit_position', edit_default, @isnumeric) % edit position; [left bottom width height]
par.addParameter('show_play_button',true, @islogical)  % array axis
par.addParameter('show_edit_box', true, @islogical) % edit box
par.addParameter('fnct', @(x)x)   % data processing function 
par.addParameter('order', 1:size(img,3), @isnumeric) % change slice order in stack
par.addParameter('plot_residua', false, @islogical) % plot residua in the image 
par.addParameter('save_movie', '', @ischar) % specify filename if a movie shall be written
par.addParameter('movie_quality', 80, @isnumeric) % movie quality


par.parse(vararg{:})
vars = par.Results;

vars.fps = vars.fps *1.2; % correct for overhead

% permute the array to slide along diferent axis
switch  vars.slider_axis
    case 1
        img = rot90(permute(img,[2,3,1]));
    case 2
        img = rot90(permute(img,[1,3,2]));
end
if any(cellfun(@(x)(strcmpi(x, 'order')),  par.UsingDefaults))
    % redefine the order just in case that the axis were swapped, but only
    % if there is  not use preference 
    vars.order = 1:size(img,3); 
end


if ~isempty(vars.title_list)
   assert(length(vars.title_list) == size(img,3), 'Number of titles has to correspond to number of frames') 
end

sz = size(img,3);

im = imhandles(gcf);
ax = gca;
if ~isprop(ax, 'index')
    ax.addprop('index');
    ax.index = length(im)+1;
else
    if isprop(ax, 'play_handle')
        delete(ax.play_handle);
    end
    if isprop(ax, 'slider_handle')
        delete(ax.slider_handle);
    end
    if isprop(ax, 'edit_handle')
        delete(ax.edit_handle);
    end
    if isprop(ax, 'vars')
        ax.vars = [];
    end
end

if sz>1
    % checks
    vars.init_frame = round(vars.init_frame); 

    if vars.init_frame > sz || vars.init_frame  < 1
        warning('Initial frame exceeds stack size.')
        vars.init_frame = 1;
    end
        
    vars.vargin = ax_img;
    
    if ~ax.isprop('img')
        ax.addprop('img');
    end

    ax.img = img;

    if ~ax.isprop('play')
        ax.addprop('play');
    end
    ax.play = @(x)play(x);

    if ~ax.isprop('stop')
        ax.addprop('stop');
    end
    ax.stop = @(x)stop(x);
    
    if ~ax.isprop('update_fig')
        ax.addprop('update_fig');
    end    
    ax.update_fig = @(x)update_fig(x);

    
    %%% set handles
    
    % slider
    slider_handle=uicontrol(gcf,'Style','slider','Max',sz,'Min',1,...
        'Value',vars.init_frame,'SliderStep',[1/(sz-1) 10/(sz-1)],...
        'Units','normalized','Position',vars.slider_position);
    if ~isprop(slider_handle, 'ax_index')
        slider_handle.addprop('ax_index');
        slider_handle.ax_index = ax.index;
    end
    if ~ax.isprop('slider_handle')
        ax.addprop('slider_handle');
        ax.slider_handle = slider_handle;
    elseif ax.isprop('slider_handle') && ~ax.slider_handle.isvalid
        ax.slider_handle = slider_handle;
    end
    
    % play button
    if vars.show_play_button
        visible_button = 'on';
    else
        visible_button = 'off';
        if vars.loop
            warning('Loop can not be aborted without buttons. Setting ''loop'' back to ''false''.');
            vars.loop = false;
        end
    end
        
    play_handle=uicontrol(gcf,'Style','pushbutton','string','Play',...
        'Units','normalized','Position',vars.play_position, 'Visible', visible_button);
    if ~isprop(play_handle, 'ax_index')
        play_handle.addprop('ax_index');
        play_handle.ax_index = ax.index;
    end
    if ~ax.isprop('play_handle')
        ax.addprop('play_handle');
        ax.play_handle = play_handle;
    elseif ax.isprop('play_handle') && ~ax.play_handle.isvalid
        ax.play_handle = play_handle;
    end
    if ~ax.isprop('vars')
        ax.addprop('vars');
        ax.vars = vars;
    else
        ax.vars = vars;
    end
    set(play_handle,'Callback',{@play_callback,ax});

    
    % text edit
    if vars.show_edit_box
        visible_box = 'on';
    else
        visible_box = 'off';
    end
    edit_handle = uicontrol('style','edit','units','normalized', 'Position', vars.edit_position, 'Visible', visible_box);
    set(edit_handle, 'Callback', {@edit_callback, ax});
    if ~ax.isprop('edit_handle')
        ax.addprop('edit_handle');
        ax.edit_handle = edit_handle;
    elseif ax.isprop('edit_handle') && ~ax.edit_handle.isvalid
        ax.edit_handle = edit_handle;
    end    
    if ~isprop(edit_handle, 'ax_index')
        edit_handle.addprop('ax_index');
        edit_handle.ax_index = ax.index;
    end

    
    % set callback functions
    set(slider_handle,'Callback',{@slider_callback,ax});
    set(edit_handle, 'String', num2str(get(ax.slider_handle,'Value')));
    
    
    if vars.autoplay
        play_callback(ax, ax, ax);
    end
    
    update_fig(ax)
    
else
    % standard imagesc should be enough
    if ax.isprop('update_title') || ax.isprop('play_handle') || ax.isprop('vars')
        if ax.isprop('vars') && isfield(ax.vars, 'slider_handle')
            ax.vars = rmfield(ax.vars, 'slider_handle');
        end
        if ax.isprop('play_handle')
            delete(ax.play_handle);
        end
        cla(ax);
    end
    
    img = gather(vars.fnct(img));

    if ~isreal(img)
        img = c2image(img);
    end
    if ~isempty(ax_img)
        imagesc(ax_img{:}, img);
    else
        imagesc(img);
    end
    if ~isempty(vars.title_list)
        title(ax, vars.title_list{1}, 'Interpreter', 'none')
    end
end

end

% plotting function
function update_fig(ax)
import math.isint
import plotting.c2image

%     im = imhandles(gcf);
    vars = ax.vars;
    slice = round(get(ax.slider_handle,'Value'));
    slice = max(1, min(length(vars.order), slice));

    % FIXME: everything works better without following lines 
    % sl = gcbo();
    % if ~isempty(sl)
    %     ax = findobj('index', sl.ax_index);
    % end

    img = gather(vars.fnct(squeeze(ax.img(:,:,vars.order(slice),:))));
    
    if ~isreal(img)
        img = c2image(img);
    end
    if vars.plot_residua
        [residua{2},residua{1}] = find(abs(utils.findresidues(img))>0.1); 
        
    end
    
    % if the current axis is empty, use imagesc with remaining arguments
    if ~ax.isprop('update_title')
        ax.addprop('update_title');
        ax.addprop('user_title');
        ax.update_title = true;
        if ~isempty(vars.vargin)
            imagesc(vars.vargin{:}, img);
        else
            imagesc(img);
        end
        hold all 
        if vars.plot_residua && ~isempty(residua{1})
            plot(residua{:},'or')
        elseif vars.plot_residua
            plot(0,0,'or')
        end
        hold off 
        addlistener(ax.Title, 'String', 'PostSet', @(gt, event)callback_title_post(ax, ax));
    else
        % if we just need to update the figure, only update the data
        ax_data = ax.findobj('Type', 'Image');
        ax_data.CData = img;
        if vars.plot_residua
            ax_data = ax.findobj('Type', 'Line');
            ax_data(1).XData = residua{1};
            ax_data(1).YData = residua{2};
        end
    end    

        
    % write title
    if isempty(vars.title_list)
        ax.update_title = false;
        if ~isempty(ax.user_title)
            title_text = sprintf(ax.user_title, vars.order(slice));
            title(ax, title_text, 'Interpreter', 'none');
        end
        ax.update_title = true;
    else
        if ~isempty(vars.title_list)
            title(ax, vars.title_list{vars.order(slice)}, 'Interpreter', 'none')
        end
    end
    
end







%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% Callback subfunctions %%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

function slider_callback(~,~,ax)
%     ob = gco;
%     vars.ax_index = ob.ax_index;
    if ax.isprop('edit_handle')
        set(ax.edit_handle, 'string', num2str(round(get(ax.slider_handle,'Value'))));
    end
    update_fig(ax)
    drawnow()

end


function callback_title_post(ax, ~, ~)
    if ax.update_title
        ax.user_title = ax.Title.String;
        try
        ax.Title.String = sprintf(ax.user_title, get(ax.slider_handle,'Value'));
        end
    end
end


function play_callback(~,~,ax)
%     ax = findobj('index', ax.slider_handle.ax_index);
    vars = ax.vars;
    if ax.isprop('slider_handle') && ax.slider_handle.isvalid
        update_slider = true;
    else
        update_slider = false;
    end
    if ax.isprop('edit_handle') && ax.edit_handle.isvalid
        update_edit = true;
    else
        update_edit = false;
    end
    try
        switch get(ax.play_handle,'string')
            case 'Play'
                if ~isempty(vars.save_movie)
                    disp(['Saving movie to  ' vars.save_movie]);
                    writeobj = VideoWriter(vars.save_movie);
                    writeobj.Quality=vars.movie_quality;
                    writeobj.FrameRate=vars.fps;
                    
                    open(writeobj);
                    vars.writeobj = writeobj;
                end
                set(ax.play_handle,'string','Stop')
                sz = size(ax.img,3);
                pos = round(get(ax.slider_handle,'Value'));
                if pos == sz
                    set(ax.slider_handle,'Value',1);
                    pos = 1;
                end
                while pos <=sz
                    
                    if strcmp(get(ax.play_handle,'string'), 'Play')
                        break
                    end
                    if update_slider
                        set(ax.slider_handle,'Value',pos)
                    end
                    if update_edit
                        set(ax.edit_handle, 'String', num2str(pos));
                    end
                    
                    update_fig(ax)
                    pause(1/vars.fps)
                    if vars.loop && pos == sz
                        pos = 1;
                    else
                        pos = pos+1;
                    end
                    if vars.save_movie
                        currFrame = getframe;
                        writeVideo(vars.writeobj,currFrame);
                    end
                    
                end
                set(ax.play_handle,'string','Play')
                if vars.reset_frame
                    set(ax.slider_handle,'Value',vars.init_frame)
                end
                if vars.save_movie
                    close(vars.writeobj);
                end
            case 'Stop'
                set(ax.play_handle,'string','Play')
                if vars.save_movie    
                    close(vars.writeobj); 
                end
        end
    catch
        if ~ax.isprop('play_handle')
            fprintf('Lost connection to figure instance.\n')
        end
    end
end

function edit_callback(~,~, ax)
    str=get(ax.edit_handle,'String');
    
    if isempty(str2num(str))
        warndlg('Input must be numerical'); 
        set(ax.edit_handle, 'string', num2str(round(get(ax.slider_handle,'Value'))));
    else
        set(ax.slider_handle,'Value',str2num(str))
        update_fig(ax)
        drawnow()
    end

end

function play(ax)
    play_callback(ax.vars, ax.vars, ax);
end

function stop(ax)
    set(ax.play_handle,'string','Play')
    set(ax.slider_handle,'Value',ax.vars.init_frame)
end