function [xPoints, yPoints, h] = plotSpikeRaster(spikes,varargin)
% PLOTSPIKERASTER Create raster plot from binary spike data or spike times
%   Efficiently creates raster plots with formatting support. Faster than
%   common implementations. Multiple plot types and parameters available!
%   Look at Parameters section below.
% 
%   Inputs:
%       M x N logical array (binary spike data):
%           where M is the number of trials and N is the number of time
%           bins with maximum of 1 spike per bin. Assumes time starts at 0.
%       M x 1 cell of spike times:
%           M is the number of trials and each cell contains a 1 x N vector
%           of spike times. Units should be in seconds.
%
%   Output:
%       xPoints - vector of x points used for the plot.
%       yPoints - vector of y points used for the plot.
%       h - returns the plot / image handle.
%
%   Parameters:
%       PlotType - default 'horzline'. Several types of plots available:
%           1. 'horzline' -     plots spikes as gray horizontal lines. 
%           2. 'vertline' -     plots spikes as vertical lines, centered
%               vertically on the trial number. 
%           3. 'scatter' -      plots spikes as gray dots.
%
%           ONLY FOR BINARY SPIKE DATA:
%           4. 'imagesc' -      plots using imagesc. Flips colormap so
%               black indicates a spike. Not affected by SpikeDuration,
%               RelSpikeStartTime, and similar timing parameters.
%           5. 'horzline2' -    more efficient plotting than horzline if
%               you have many timebins, few trials, and high spike density.
%               Note: SpikeDuration parameter DOES NOT WORK IF LESS THAN 
%               TIME PER BIN.
%           6. 'vertline2' -    more efficient plotting than vertline if
%               you have few timebins, many trials, and high spike density.
%           Note: Horzline and vertline should be fine for most tasks.
% 
%       FigHandle - default gcf (get current figure).
%           Specify a specific figure or subplot to plot in. If no figure
%           is specified, plotting will occur on the current figure. If no
%           figure is available, a new figure will be created.
% 
%       LineFormat - default line is gray. Used for 'horzline' and
%           'vertline' plots only. Usage example:
%               LineFormat = struct()
%               LineFormat.Color = [0.3 0.3 0.3];
%               LineFormat.LineWidth = 0.35;
%               LineFormat.LineStyle = ':';
%               plotSpikeRaster(spikes,'LineFormat',LineFormat)
% 
%       MarkerFormat - default marker is a gray dot with size 1. Used for
%           scatter type plots only. Usage is the same as LineFormat.
% 
%       AutoLabel - default 0.
%           Automatically labels x-axis as 'Time (ms)' or 'Time (s)' and
%           y-axis as 'Trial'.
% 
%       XLimForCell - default [NaN NaN].
%           Sets x-axis window limits if using cell spike time data. If
%           unchanged, the default limits will be 0.05% of the range. For
%           better performance, this parameter should be set.
%
%       TimePerBin - default 0.001 (1 millisecond).
%           Sets the duration of each timebin for binary spike train data.
%
%       TrialOffset - default 0.
%           Offset to add to the trial number (in the y axis)
%
%       SpikeDuration - default 0.001 (1 millisecond).
%           Sets the horizontal spike length for cell spike time data.
%
%       RelSpikeStartTime - default 0 seconds.
%           Determines the starting point of the spike relative to the time
%           indicated by spike times or time bins. For example, a relative
%           spike start time of -0.0005 would center 1ms spikes for a
%           horzline plot of binary spike data.
%
%       rasterWindowOffset - default NaN
%           Exactly the same as relSpikeStartTime, but unlike
%           relSpikeStartTime, the name implies that it can be used to make
%           x-axis start at a certain time. If set, takes precedence over
%           relSpikeStartTime.
%       
%       VertSpikePosition - default 0 (centered on trial).
%           Determines where the spike position is relative to the trial. A
%           value of 0 is centered on the trial number - so a spike on
%           trial 3 would have its y-center on 3. Example: A common type of
%           spike raster plots vertical spikes from previous trial to
%           current trial. Set VertSpikePosition to -0.5 to center the
%           spike between trials.
%
%       VertSpikeHeight - default 1 (spans 1 trial).
%           Determines height of spike for 'vertline' plots. Decrease to
%           separate trials with a gap.
%
%   Examples:
%       plotSpikeRaster(spikeTimes);
%               Plots raster plot with horizontal lines.
%
%       plotSpikeRaster(spikeTimes,'PlotType','vertline');
%               Plots raster plot with vertical lines.
%
%       plotSpikeRaster(spikeTimes,'FigHandle',h,'AutoLabel',1,...
%           'XLimForCell',[0 10],'HorzSpikeLength',0.002,); 
%               Plots raster plot on figure with handle h using horizontal
%               lines of length 0.002, with a window from 0 to 10 seconds,
%               and automatic labeling.
%
%       plotSpikeRaster(spikeTimes,'PlotType','scatter',...
%           'MarkerFormat',MarkerFormat);
%               Plots raster plot using dots with a format specified by
%               MarkerFormat.


%% AUTHOR    : Jeffrey Chiou
%% $DATE     : 07-Feb-2014 12:15:47 $
%% $Revision : 1.2 $
%% DEVELOPED : 8.1.0.604 (R2013a)
%% FILENAME  : plotSpikeRaster.m
%% Modified by Javier G. Orlandi 2018 for NETCAL (added trialOffset)

%% Set Defaults and Load optional arguments
LineFormat.Color = [0.2 0.2 0.2];
MarkerFormat.MarkerSize = 1;
MarkerFormat.Color = [0.2 0.2 0.2];
MarkerFormat.LineStyle = 'none';
h = [];

p = inputParser;
p.addRequired('spikes',@(x) islogical(x) || iscell(x));
p.addParamValue('FigHandle',gcf,@isinteger);
p.addParamValue('PlotType','horzLine',@ischar);
p.addParamValue('LineFormat',LineFormat,@isstruct)
p.addParamValue('MarkerFormat',MarkerFormat,@isstruct);
p.addParamValue('AutoLabel',0, @islogical);
p.addParamValue('XLimForCell',[NaN NaN],@(x) isnumeric(x) && isvector(x));
p.addParamValue('TimePerBin',0.001,@(x) isnumeric(x) && isscalar(x));
p.addParamValue('TrialOffset',0,@(x) isnumeric(x) && isscalar(x));
p.addParamValue('SpikeDuration',0.001,@(x) isnumeric(x) && isscalar(x));
p.addParamValue('RelSpikeStartTime',0,@(x) isnumeric(x) && isscalar(x));
p.addParamValue('RasterWindowOffset',NaN,@(x) isnumeric(x) && isscalar(x));
p.addParamValue('VertSpikePosition',0,@(x) isnumeric(x) && isscalar(x));
p.addParamValue('VertSpikeHeight',1,@(x) isnumeric(x) && isscalar(x));
p.parse(spikes,varargin{:});

spikes = p.Results.spikes;
figH = p.Results.FigHandle;
plotType = lower(p.Results.PlotType);
lineFormat = struct2opt(p.Results.LineFormat);
markerFormat = struct2opt(p.Results.MarkerFormat);
autoLabel = p.Results.AutoLabel;
xLimForCell = p.Results.XLimForCell;
timePerBin = p.Results.TimePerBin;
trialOffset = p.Results.TrialOffset;
spikeDuration = p.Results.SpikeDuration;
relSpikeStartTime = p.Results.RelSpikeStartTime;
rasterWindowOffset = p.Results.RasterWindowOffset;
vertSpikePosition = p.Results.VertSpikePosition;
vertSpikeHeight = p.Results.VertSpikeHeight;

if ~isnan(rasterWindowOffset) && relSpikeStartTime==0
    relSpikeStartTime = rasterWindowOffset;
elseif ~isnan(rasterWindowOffset) && relSpikeStartTime~=0
    disp(['Warning: RasterWindoWOffset and RelSpikeStartTime perform the same function. '...
        'The value set in RasterWindowOffset will be used over RelSpikesStartTime']);
    relSpikeStartTime = rasterWindowOffset;
end

%% Initialize figure and begin plotting logic
figure(figH);
hold on;

if islogical(spikes)
    %% Binary spike train matrix case. Initialize variables and set axes.
    nTrials = size(spikes,1);  
    nTimes = size(spikes,2);
    % Convert Parameters to correct units using TimePerBin. Default is 1 ms
    % per bin (0.001s)
    spikeDuration = spikeDuration/timePerBin;
    relSpikeStartTime = relSpikeStartTime/timePerBin;
    
    % Note: xlim and ylim are much, much faster than axis or set(gca,...).
    xlim([0+relSpikeStartTime nTimes+1+relSpikeStartTime]);
    ylim([0 nTrials+1]+trialOffset);        
    
    switch plotType
        case 'horzline'
            %% Horizontal Lines
            % Find the trial (yPoints) and timebin (xPoints) of each spike
            [trials,timebins] = find(spikes);
            trials = trials';
            timebins = timebins';

            xPoints = [ timebins + relSpikeStartTime;
                        timebins + relSpikeStartTime + spikeDuration;
                        NaN(size(timebins)) ];
            yPoints = [ trials + vertSpikePosition;
                        trials + vertSpikePosition;
                        NaN(size(trials)) ];
                    
            xPoints = xPoints(:);
            yPoints = yPoints(:);
            h = plot(xPoints,yPoints+trialOffset,'k',lineFormat{:});
        case 'vertline'
            %% Vertical Lines
            % Find the trial (yPoints) and timebin (xPoints) of each spike
            [trials,timebins] = find(spikes);
            trials = trials';
            timebins = timebins';
            halfSpikeHeight = vertSpikeHeight/2;
            
            xPoints = [ timebins + relSpikeStartTime;
                        timebins + relSpikeStartTime;
                        NaN(size(timebins)) ];
            yPoints = [ trials - halfSpikeHeight + vertSpikePosition;
                        trials + halfSpikeHeight + vertSpikePosition;
                        NaN(size(trials)) ];

            xPoints = xPoints(:);
            yPoints = yPoints(:);
            h = plot(xPoints,yPoints+trialOffset,'k',lineFormat{:});
        case 'horzline2'
            %% Horizontal lines, for many timebins
            % Plots a horizontal line the width of a time bin for each
            % spike. Efficient for fewer trials and many timebins. 
            xPoints = [];
            yPoints = [];
            
            for trials = 1:nTrials
                % If there are spikes, plot a line. Otherwise, do nothing.
                if sum(spikes(trials,:)) > 0
                    
                    % Find the difference in spike times. Padding at the
                    % front and back with a zero accounts for spikes in
                    % the first and last indices, and keeps startY and endY
                    % the same size
                    spikeDiff = diff([0 spikes(trials,:) 0]);
                    % Ex. [0 1 1] -> [0 0 1 1 0]. diff(...) -> [0 1 0 -1]
                    
                    % Find line segments to plot (line segments longer than
                    % one trial are for spikes on consecutive trials)
                    startX = find(spikeDiff > 0);
                    % Ex. cont. from above: find(...) -> 2
                    endX = find(spikeDiff < 0);
                    % Ex. cont. from above: find(...) -> 4. Thus a line
                    % segment will be defined from 2 to 4 (x-axis)
                    
                    % Combine x points and adjust x points according to
                    % parameters. Add Add NaNs to break up line segments.
                    trialXPoints = [startX + relSpikeStartTime;...
                        endX + relSpikeStartTime + (spikeDuration - 1);...
                        NaN(size(startX)) ];
                    % Explanation for 'spikeDuration - 1': spikeDuration
                    % has been converted already using timePerBin, so the
                    % offset at the end is simply duration - one timeBin,
                    % since 1 timebin's worth of spikes has passed. Only
                    % affects last spike if less than time per bin.

                    % Convert x points array to vector
                    trialXPoints = trialXPoints(:)';
                    
                    % Add y points centered on the trial by default
                    % (adjustable with vertSpikePosition parameter)
                    trialYPoints = trials*ones(size(trialXPoints)) + vertSpikePosition;
                    
                    % Store this trial's x and y points. Unfortunately,
                    % preallocating and trimming may actually be slower,
                    % depending on data.
                    xPoints = [xPoints trialXPoints];
                    yPoints = [yPoints trialYPoints];
                    
                end
            end
            
            h = plot(xPoints, yPoints+trialOffset,'k', lineFormat{:});
            
        case 'vertline2'
            %% Vertical lines, for many trials
            % Plot one long line for each timebin. Reduces the number of
            % objects to plot. Efficient for many trials and fewer timebins            
            xPoints = [];
            yPoints = [];
            
            for time = 1:nTimes
                if sum(spikes(:,time)) > 0
                    
                    % Find the difference in spike times. Same principle as
                    % horzline2. See comments above for explanation.
                    spikeDiff = diff([0 spikes(:,time)' 0]);
                    
                    % Find line segments to plot (line segments longer than
                    % one trial are for spikes on consecutive trials)
                    startY = find(spikeDiff > 0);
                    endY = find(spikeDiff < 0);
                    
                    % Add NaNs to break up line segments
                    timeBinYPoints = [startY + vertSpikePosition;...
                        endY + vertSpikePosition; NaN(size(startY))];
                    % Convert to vector
                    timeBinYPoints = timeBinYPoints(:)';
                    timeBinXPoints = time*ones(size(timeBinYPoints));
                    
                    % Store this timebin's x and y points.
                    xPoints = [xPoints timeBinXPoints];
                    % Subtract 0.5 from each y point so spikes are centered
                    % by default (adjustable with vertSpikePosition)
                    yPoints = [yPoints timeBinYPoints-0.5];

                end
            end
            
            h = plot(xPoints, yPoints+trialOffset, 'k', lineFormat{:});
            
        case 'scatter'
            %% Dots or other markers (scatterplot style)
            % Find the trial (yPoints) and timebin (xPoints) of each
            % spike
            [yPoints,xPoints] = find(spikes==1);
            xPoints = xPoints + relSpikeStartTime;
            h = plot(xPoints,yPoints+trialOffset,'.k',markerFormat{:});
            
        case 'imagesc'
            %% Imagesc
            h = imagesc(spikes);
            % Flip the colormap since the default is white for 1, black for
            % 0.
            colormap(flipud(colormap('gray')));
            
        otherwise
            error('Invalid plot type. Must be horzline, vertline, horzline2, vertline2, scatter, or imagesc');
    end % switch
    set(gca,'YDir','reverse');
    
    %% Label
    if autoLabel
        xlabel('Time (ms)');
        ylabel('Trial');
    end

else % Equivalent to elseif iscell(spikes).
    %% Cell case
    
    % Validation: First check to see if cell array is a vector, and each
    % trial within is a vector.
    if ~isvector(spikes)
        error('Spike cell array must be an M x 1 vector.')
    end
    trialIsVector = cellfun(@isvector,spikes);
    if sum(trialIsVector) < length(spikes)
        error('Cells must contain 1 x N vectors of spike times.');
    end
    
    % Now make sure cell array is M x 1 and not 1 x M.
    if size(spikes,2) > 1 && size(spikes,1) == 1
        spikes = spikes';
    end
    
    % Make sure each trial is 1 x N and not N x 1
    nRowsInTrial = cellfun(@(x) size(x,1),spikes);
    % If there is more than 1 row in any trial, add a warning, and
    % transpose those trials. Allows for empty trials/cells (nRows > 1
    % instead of > 0).
    if sum(nRowsInTrial > 1) > 0 
        trialsToReformat = find(nRowsInTrial > 1);
        disp('Warning - some cells (trials) have more than 1 row. Those trials will be transposed.');
        for t = trialsToReformat
            spikes{trialsToReformat} = spikes{trialsToReformat}';
        end
    end
    
    nTrials = length(spikes);
    
    % Find x-axis limits that aren't manually set (default [NaN NaN]), and
    % automatically set them. This is because we don't assume spikes start
    % at 0 - we can have negative spike times.
    limitsToSet = isnan(xLimForCell);
    if sum(limitsToSet) > 0
        % First find range of spike times
        minTimes = cellfun(@min,spikes,'UniformOutput',false);
        minTime = min( [ minTimes{:} ] );
        maxTimes = cellfun(@max,spikes,'UniformOutput',false);
        maxTime = max( [ maxTimes{:} ] );
        timeRange = maxTime - minTime;
        
        % Find 0.05% of the range.
        xStartOffset = relSpikeStartTime - 0.0005*timeRange;
        xEndOffset = relSpikeStartTime + 0.0005*timeRange + spikeDuration;
        newLim = [ minTime+xStartOffset, maxTime+xEndOffset ];
        xLimForCell(limitsToSet) = newLim(limitsToSet);
        % End result, if both limits are automatically set, is that the x
        % axis is expanded 0.1%, so you can see initial and final spikes.
    end
    xlim(xLimForCell);
    ylim([0 nTrials+1]+trialOffset);
    
    if strcmpi(plotType,'vertline') || strcmpi(plotType,'horzline')
        %% Vertical or horizontal line logic
        nTotalSpikes = sum(cellfun(@length,spikes));
        
        % Preallocation is possible since we know how many points to
        % plot, unlike discrete case. 3 points per spike - the top pt,
        % bottom pt, and NaN.
        xPoints = NaN(nTotalSpikes*3,1);
        yPoints = xPoints;
        currentInd = 1;
        
        if strcmpi(plotType,'vertline')
            %% Vertical Lines
            halfSpikeHeight = vertSpikeHeight/2;
            for trials = 1:nTrials
                nSpikes = length(spikes{trials});
                nanSeparator = NaN(1,nSpikes);
                
                trialXPoints = [ spikes{trials} + relSpikeStartTime;...
                    spikes{trials} + relSpikeStartTime; nanSeparator ];
                trialXPoints = trialXPoints(:);
                
                trialYPoints = [ (trials-halfSpikeHeight)*ones(1,nSpikes);...
                    (trials+halfSpikeHeight)*ones(1,nSpikes); nanSeparator ];
                trialYPoints = trialYPoints(:);
                
                % Save points and update current index
                xPoints(currentInd:currentInd+nSpikes*3-1) = trialXPoints;
                yPoints(currentInd:currentInd+nSpikes*3-1) = trialYPoints;
                currentInd = currentInd + nSpikes*3;
            end
            
        else % (if plotType is 'horzline')
            %% Horizontal Lines
            for trials = 1:nTrials
                nSpikes = length(spikes{trials});                
                nanSeparator = NaN(1,nSpikes);
                
                trialXPoints = [ spikes{trials} + relSpikeStartTime;...
                    spikes{trials} + relSpikeStartTime + spikeDuration;...
                    nanSeparator ];
                trialXPoints = trialXPoints(:);
                
                trialYPoints = [ trials*ones(1,nSpikes);...
                    trials*ones(1,nSpikes); nanSeparator ];
                trialYPoints = trialYPoints(:);
                
                % Save points and update current index
                xPoints(currentInd:currentInd+nSpikes*3-1) = trialXPoints;
                yPoints(currentInd:currentInd+nSpikes*3-1) = trialYPoints;
                currentInd = currentInd + nSpikes*3;
            end
            
        end
        
        % Plot everything at once! We will reverse y-axis direction later.
        h = plot(xPoints, yPoints+trialOffset, 'k', lineFormat{:});
        
    elseif strcmpi(plotType,'scatter')
        %% Dots or other markers (scatterplot style)
        % Combine all spike times into one vector
        xPoints = [ spikes{:} ];
        
        % Getting the trials is trickier. 3 steps:
        % 1. First convert all the spike times into ones.
        trials = cellfun( @(x) {ones(size(x))}, spikes );
        % 2. Then multiply by trial number.
        for trialNum = 1:length(spikes)
            trials{trialNum} = trialNum*trials{trialNum};
        end
        % 3. Finally convert into a vector
        yPoints = [ trials{:} ];
        
        % Now we can plot! We will reverse y-axis direction later.
        h = plot(xPoints,yPoints+trialOffset,'.k',markerFormat{:});
        
    elseif strcmpi(plotType,'imagesc') || strcmpi(plotType,'vertline2') || strcmpi(plotType,'horzline2')
        error('Can''t use imagesc/horzline2/vertline2 with cell array. Use with logical array of binary spike train data.');
    else
        error('Invalid plot type. With cell array of spike times, plot type must be horzline, vertline, or scatter.');
    end % plot type switching
    
    %% Reverse y-axis direction and label
    set(gca,'YDir','reverse');
    if autoLabel
        xlabel('Time (s)');
        ylabel('Trial');
    end
    
end % logical vs cell switching

%% Figure formatting
% Draw the tick marks on the outside
set(gca,'TickDir','out') 

% Use special formatting if there is only a single trial.
% Source - http://labrigger.com/blog/2011/12/05/raster-plots-and-matlab/
if size(spikes,1) == 1
    set(gca,'YTick', [])                        % don't draw y-axis ticks
    set(gca,'PlotBoxAspectRatio',[1 0.05 1])    % short and wide
    set(gca,'YColor',get(gcf,'Color'))          % hide the y axis
    ylim([0.5 1.5])
end

hold off;

end % main function

function paramCell = struct2opt(paramStruct)
% Converts structure to parameter-value pairs
%   Example usage:
%       formatting = struct()
%       formatting.color = 'black';
%       formatting.fontweight = 'bold';
%       formatting.fontsize = 24;
%       formatting = struct2opt(formatting);
%       xlabel('Distance', formatting{:});
% Adapted from:
% http://stackoverflow.com/questions/15013026/how-can-i-unpack-a-matlab-structure-into-function-arguments
% by user 'yuk'

fname = fieldnames(paramStruct);
fval = struct2cell(paramStruct);
paramCell = [fname, fval]';
paramCell = paramCell(:);

end % struct2opt