% function [varargout] = spec_read(specDatFile, varargin)
% call 'spec_read()' for further help
%
% please reports bugs, problems, suggestions for improvements to:
% CXS group
%
% the case of duplicate scannumbers in the spec file, and how to
%   address them remains to be implemented

%*-----------------------------------------------------------------------*
%|                                                                       |
%|  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 [varargout] = spec_read(specDatFile, varargin)
import io.*
import utils.find_files

% check minimum number of input arguments
  if(~isunix)
    error(['The function ''spec_read'' relies on various system calls,\n' ...
      '  currently available only on UNIX-like systems\n']);
  end
  if (nargin<1) 
    spec_help(mfilename);
    error('At least the spec data filename has to be specified as input parameter.')
  end
  
% accept cell array with name/value pairs as well
  no_of_in_arg = nargin;
  if (nargin == 2)
    if (isempty(varargin))
      % ignore empty cell array
      no_of_in_arg = no_of_in_arg -1;
    else
      if (iscell(varargin{1}))
        % use a filled one given as first and only variable parameter
        varargin = varargin{1};
        no_of_in_arg = 1 + length(varargin);
      end
    end
  end
% check number of input arguments
  if (rem(no_of_in_arg,2) ~= 1)
      error('The optional parameters have to be specified as ''name'',''value'' pairs');
  end
  
% here the default parameters  
  scannr     = -1;
  output_arg = {'meta','motor','counter'};
  unhandled_par_error = 1;
  force_cell = 0;
% other parameters
  burstn0 = 1;
  mexpn0  = 1;
  creader = 1;
  orchestraPath = [];
  specReadOptions = [];
  nexus = 1;
  
% parse the variable input arguments
  vararg = cell(0,0);
  for jj = 1:2:length(varargin)
    name = varargin{jj};
    value = varargin{jj+1};
    switch name
      case 'ScanNr'
        if (~all(value==round(value)) || any(value==0))
          error('The value of ''%s'' needs to be a (vector of) integer(s) not equal to zero', name)
        end
        scannr = value;
      case 'Burst'
        if (~all(value==round(value)) || any(value<=0))
          error('The value of ''%s'' needs to be a positive integer', name)
        end
        burstn0 = max(1,value);
        mexpn0 = 1;
        creader = 0;
        nexus = 0;
      case 'MultExp'
        if (~all(value==round(value)) || any(value<=0))
          error('The value of ''%s'' needs to be a positive integer', name)
        end
        burstn0 = 1;
        mexpn0 = max(1,value);
        creader = 0;
        nexus = 0;
      case 'OutPut'
        output_arg = set_TextFlag(output_arg,value);
        nexus = 0;
      case 'PilatusMask'
        pilatusMask = value;
        output_arg = set_TextFlag(output_arg,'+pilatus');
        creader = 0;
        nexus = 0;
      case 'PilatusDir'
        pilatusDir0 = value;
        output_arg = set_TextFlag(output_arg,'+pilatus');
        creader = 0;
        nexus = 0;
      case 'Cell'
        force_cell = value;
      case 'CReader'
        creader = value;
      case 'Orchestra'
        orchestraPath = value;
        nexus = 0;
      case 'UnhandledParError'
        unhandled_par_error = value;
      otherwise
        vararg{end+1} = name;                                   %#ok<AGROW>
        vararg{end+1} = value;                                  %#ok<AGROW>
    end
  end
  
  try  
      specDatFile = beamline.find_specDatFile(specDatFile);
  catch ME
      % find ~/Data10 if spec file was provided
      if nexus
          warning('Could not find spec file.')
      else
          ME.rethrow();
      end
      try
          if contains(specDatFile, 'specES1')
              res = strsplit(specDatFile, 'specES1');
              cpath = res{1};
          else
              cpath = specDatFile;
          end
          
          for ii=1:numel(scannr)
              res = dir(fullfile(cpath, 'data', utils.compile_x12sa_dirname(scannr(ii)), sprintf('*%05d*.h5', scannr(ii))));
              if ~isempty(res)
                  vararg{ii} = io.HDF.hdf5_load(fullfile(res(1).folder, res(2).name), '/entry/collection/specES1');
              else
                  vararg{ii} = [];
              end
          end
          if all(isempty(vararg{:}))
              error('Could not find nexus file.')
          end
          if (length(vararg)==1 && ~force_cell)
              varargout{1} = vararg{1};
          elseif iscell(vararg)
              varargout{1} = vararg;
          else
              varargout{1} = num2cell(vararg);
          end
          warning('Using nexus data.');
          return
      catch
          ME.rethrow();
      end
  end
  
  if creader
      try
          if ~isempty(orchestraPath)
              specReadOptions = [specReadOptions ' --orchestra ' orchestraPath];
          end
          sysCall = [ fullfile(fileparts(mfilename('fullpath')), './spec_reader/spec_reader') ' -s ' specDatFile ' --scanNr ' strjoin(arrayfun(@(x) num2str(x),scannr,'UniformOutput',false),',') specReadOptions];
          [stat, out] = system(sysCall);
          if stat
              error(['Fast spec reader failed: ' strtrim(out)])
          end
          res = jsondecode([ out ]);
          if (length(res)==1 && ~force_cell)
              varargout{1} = res;
          elseif iscell(res)
              varargout{1} = res';
          else
              varargout{1} = num2cell(res)';
          end
      catch ME
          if creader
              warning('Fast spec reader failed.')
              disp(ME)
              creader = 0;
          end
      end
  end
  if ~creader
      % reading the spec file for the scans
      if ~exist(specDatFile, 'file')
          error('Spec file was not found in %s', specDatFile)
      end
      [scans,scanline] = spec_readscan(specDatFile, scannr);
      
      % reading the spec file for configuration information
      [motorc,motorline] = spec_readconfig(specDatFile);
      motorn = cell(size(scannr));
      for jj=1:numel(scans)
          motorn{jj} = motorc{find(motorline<scanline(1),1,'last')};
      end
      
      
      varargout{1} = cell(size(scans));
      % copying vararg in order to keep its content for each iteration
      vararg_1 = vararg;
      vararg_1{end+1} = 'UnhandledParError';
      vararg_1{end+1} = 0;
      for jj=1:numel(scans)
          scanstr = scans{jj}';
          arrout = regexp(scanstr{1},' +','split');
          scannr(jj) = str2double(arrout{2});
          
          % get the motors
          line = ~cellfun(@isempty,regexp(scanstr,'^#P'));
          arrout = reshape(transpose(strvcat((scanstr(line)))),1,[]);
          arrout = regexp(arrout,' +|(#P[0-9]+)','split');
          motorv = str2double(arrout(~cellfun(@isempty,arrout)));
          
          % get the counters
          line = ~cellfun(@isempty,regexp(scanstr,'^#L'));
          counters = regexp(scanstr{line},' +','split');
          counters = counters(2:end);
          
          % get the data
          line  = ~cellfun(@isempty,regexp(scanstr,'^[0-9+-]'));
          data = cellfun(@str2num,scanstr(line),'UniformOutput',false);
          data = cell2mat(data(:));
          
          
          % get Pilatus information if required
          if (exist('pilatusMask','var'))
              if (exist('pilatusDir0','var'))
                  pilatusDir = find_pilatusDir(pilatusDir0,scannr(jj));
              else
                  pilatusDir = '';
              end
              
              [pilatusDir, pilatusName, vararg] = ...
                  find_files(strcat(pilatusDir,pilatusMask), vararg_1);
              
              pilatusInd = zeros(numel(pilatusName),2);
              for ii=1:numel(pilatusName)
                  arrout = regexp(pilatusName(ii).name,'[_\.]','split');
                  pilatusInd(ii,:) = str2double(arrout(end-2:end-1));
              end
          end
          
          % make a structure
          scan_structure = struct;          
          if (any(strcmp(output_arg,'motor')) || ...
                  any(strcmp(output_arg,'motors')))
              for ii=1:numel(motorn{jj})
                  scan_structure.(motorn{jj}{ii}) = motorv(ii);
              end
          end
          if (any(strcmp(output_arg,'meta')))
              scan_structure.S = scanstr{1};
              
              % get the time stamp and convert it to a MATLAB-compatible date string
              line = ~cellfun(@isempty,regexp(scanstr,'^#D'));
              datestr = regexp(scanstr{line},' +','split');
              scan_structure.D = sprintf('%s-%s-%s %s', ...
                  datestr{4}, ...
                  datestr{3}, ...
                  datestr{6}, ...
                  datestr{5});
              
              line = ~cellfun(@isempty,regexp(scanstr,'^#C meta'));
              meta = scanstr(line);
              for ii=1:length(meta)
                  metaEntry = strsplit(strtrim(meta{ii}), ' ');
                  switch metaEntry{4}
                      case {'int', 'float'}
                          converterFunc = @str2double;
                      case 'string'
                          converterFunc = @(x) x;
                  end
                  
                  tmp = [];
                  for jjj=1:str2double(metaEntry{5})
                      tmp = [tmp converterFunc(metaEntry{5+jjj})];
                  end
                  scan_structure.(metaEntry{3}) = tmp;
              end
                          
              
          end        
          if (any(strcmp(output_arg,'counter')) || ...
                  any(strcmp(output_arg,'counters')))
              if (numel(data)>0)
                  for ii=1:numel(counters)
                      scan_structure.(counters{ii}) = data(:,ii);
                  end
              end
          end
          
          if (any(strcmp(output_arg,'pilatus')))
              if (burstn0>1)
                  burstn = burstn0;
              elseif (isfield(scan_structure,'burstn'))
                  burstn = max(ones(size(scan_structure.burstn)),scan_structure.burstn);
              else
                  burstn = burstn0;
              end
              if (mexpn0>1)
                  mexpn = mexpn0;
              elseif (isfield(scan_structure,'mexpn'))
                  mexpn = max(ones(size(scan_structure.burstn)),scan_structure.mexpn);
              else
                  mexpn = mexpn0;
              end
              
              if (numel(burstn)==1 && numel(mexpn)==1)
                  % these pseudo motors shouldn't be scanned and are
                  %   therefore presumably constant
                  Pilatus = cell(size(data,1),1);
                  scanInd = [transpose(floor((0:size(data,1)-1)/mexpn)), ...
                      transpose(mod(0:size(data,1)-1,mexpn))];
                  for ii=1:size(data,1)
                      if (burstn<=1)
                          indices = all(ones(size(pilatusName))'*scanInd(ii,:)==pilatusInd,2);
                      else
                          indices = all(ones(size(pilatusName))'*scanInd(ii,1)==pilatusInd(:,1),2);
                      end
                      Pilatus{ii} = {pilatusName(indices).name};
                      if (numel(indices)<burstn)
                          % accounting for missing frames
                          % untested, as of Dec 16
                          tmp = Pilatus{ii};
                          Pilatus{ii} = cell(1,burstn);
                          for kk=1:numel(tmp)
                              Pilatus{ii}{pilatusInd(2,kk)} = tmp{kk};
                          end
                      end
                  end
              end
              scan_structure.Pilatus = Pilatus;
              scan_structure.PilatusDir = pilatusDir;
          end
          
          % append orchestra data
          if ~isempty(orchestraPath)
              [config, data] = readOrchestraFile(orchestraPath, scannr(jj));
              for oidx=1:length(config)
                  scan_structure.(config{oidx}) = data{oidx};
              end
          end
          varargout{1}{jj} = scan_structure;
      end

      if (numel(varargout{1})==1 && ~force_cell)
          varargout{1} = varargout{1}{1};
      end

  end
  if ((unhandled_par_error) && (~isempty(vararg)))
      vararg                                                      %#ok<NOPRT>
      error('Not all named parameters have been handled.');
  end
  varargout{2} = vararg;
  return
end

%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function [varargout] = spec_readscan(specDatFile, scannr)
import io.*
import utils.*
% check minimum number of input arguments
  if (nargin<2) 
    spec_read_help(mfilename);
    error('usage ''spec_readscan(specDatFile, scannr)''');
  end
  
  cmd = sprintf('grep -n ''#S '' %s', specDatFile);
  [~,sysout] = system(cmd);
  arrout = regexp(sysout,'[:\n]','split');
  arrout = arrout(~cellfun(@isempty,arrout));
  arrout = transpose(reshape(arrout,2,[]));
  [s1,~] = size(arrout);
  
  if (any(scannr>0))
    tmp = regexp(arrout(:,2),' +','split');
    scanID = zeros(size(tmp));
    for ii=1:numel(tmp)
      scanID(ii) = str2double(tmp{ii}{2});
    end
  end
  
% creating a look up table where the scans begin and end
  lineI = zeros(size(scannr));
  scans = cell(size(scannr));
  for ii=1:numel(scannr)
    if (scannr(ii)<0)
      if (-scannr(ii)>size(arrout,1))
        error('There does not seem to be sufficiently many scans in ''%s''', ...
          specDatFile);
      end
      tmp = regexp(arrout(end+scannr(ii)+1,:),' +','split');
      lineI(ii) = str2double(tmp{1});
      if (scannr(ii) < -1)
        tmp = regexp(arrout(end+scannr(ii)+2,:),' +','split');
        lineF = str2double(tmp{1})-1;
      else
        cmd = sprintf('wc -l %s', specDatFile);
        [~,sysout] = system(cmd);
        lineF = str2double(regexp(sysout,'[0-9]+ ','match'));
      end
      scannr(ii) = str2double(tmp{2}(2));
    else
      tmp = find((scannr(ii)==scanID),1,'last');
      try 
          verbose(2,'reading ''%s''',arrout{tmp,2});
      catch
          fprintf('reading ''%s''\n',arrout{tmp,2});
      end

      if (isempty(tmp))
        fprintf('Do not find scan #S %d in ''%s''.\n', ...
          scannr(ii),specDatFile);
        continue
      end
      lineI(ii) = str2double(arrout(tmp,1));
      if (tmp<s1)
        lineF = str2double(arrout(tmp+1,1))-1;
      else
        cmd = sprintf('wc -l %s', specDatFile);
        [~,sysout] = system(cmd);
        lineF = str2double(regexp(sysout,'[0-9]+ ','match'));
      end
    end

    cmd = sprintf('head -n +%d %s | tail -n -%d', ...
      lineF, specDatFile, lineF+1-lineI(ii));

  % read the scan
    [~,sysout] = system(cmd);
    scans{ii} = regexp(sysout,'\n','split');
    scans{ii} = scans{ii}(~cellfun(@isempty,scans{ii}));
  end
  varargout{1} = scans;
  varargout{2} = lineI;
end
%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function [motors,linenr] = spec_readconfig(specDatFile)
import io.*
import utils.find_files
  % finding the end of the last config
  cmd = sprintf('grep -nE ''^#O[0-9]+ '' %s', ...
      specDatFile);
  [~,sysout] = system(cmd);
  arrout = regexp(sysout,'[:\n]','split');
  arrout = arrout(~cellfun(@isempty,arrout));
  arrout = transpose(reshape(arrout,2,[]));
  [s1,~] = size(arrout);
  
  % devide this collection of configuration information into single chuncks
  linenr = '';
  motors = cell(0);
  for ii=1:s1
    if (~isempty(regexp(arrout{ii,2},'^#O0+','match')))
      if (isempty(linenr))
        linenr = str2double(arrout{ii,1});
      else
        linenr = vertcat(linenr, ...
          str2double(arrout{ii,1}));
      end
      motors{numel(linenr)} = regexp(arrout{ii,2},'(^#O[0-9]+ *)| *','split');
    else
      motors{numel(linenr)} = horzcat(motors{numel(linenr)}, ...
        regexp(arrout{ii,2},'(^#O[0-9]+ *)| *','split'));
    end
  end
  for ii=1:numel(linenr)
    motors{ii} = motors{ii}(~cellfun(@isempty,motors{ii}));
  end
end

%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function pilatusDir = find_pilatusDir(pilatusDir,scannr)
import io.*
import utils.find_files
  while (any(exist(pilatusDir,'dir') == [2,7]))
% if the variable pilatusDir is not a complete path to a file
%   try to guess where a spec data file can be found, by
%   - look for directories called 'spec' or 'dat-files'
%   - look for files called '*.dat'
%   - take the newest one
    compare_str = pilatusDir;
    fname = dir(pilatusDir);
    if (exist(pilatusDir,'dir'))
      if (pilatusDir(end) ~= '/')
        pilatusDir = strcat(pilatusDir,'/');
      end
      
      for ii=1:numel(fname)
        if (strcmp(fname(ii).name,sprintf('S%02d000-%02d999',floor(scannr/1e3),floor(scannr/1e3))) || ...
            strcmp(fname(ii).name,sprintf('S%05d',scannr)))
          pilatusDir = strcat(pilatusDir,fname(ii).name);
          fname = [];
          break;
        end
      end
      for ii=1:numel(fname)
        if (strcmp(fname(ii).name,'pilatus'))
          pilatusDir = strcat(pilatusDir,fname(ii).name);
          break;
        end
      end
    end
    if (strcmp(pilatusDir,compare_str))
      break
    end
  end
end
%% %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
function [flags] = set_TextFlag(flags,value)
import io.*
import utils.find_files
  if (~iscell(value))
    value = {value};
  end
  for ii=1:numel(value)
    value_i = value{ii};
    if (regexp(value_i,'^[a-zA-Z]+$'))
      flags = {value_i};
      if (ii+1<=numel(value))
        value{ii+1} = regexprep(value{ii+1},'^([a-zA-Z]*)','+$1');
      end
    elseif (regexp(value_i,'^\+[a-zA-Z]+$'))
      flags = unique([flags,value_i(2:end)]);
      if (ii+1<=numel(value))
        value{ii+1} = regexprep(value{ii+1},'^([a-zA-Z]*)','+$1');
      end
    elseif (regexp(value_i,'^-[a-zA-Z]+$'))
      flags = flags(~strcmp(flags,value_i(2:end)));
      if (ii+1<=numel(value))
        value{ii+1} = regexprep(value{ii+1},'^([a-zA-Z]*)','-$1');
      end
    end
  end
end

function [config, data] = readOrchestraFile(orchestraPath, scanNr)
config = [];
data = [];
fname = fullfile(orchestraPath, sprintf('scan_%05d.dat', scanNr));
if ~exist(fname, 'file')
    warning('Could not find any orchestra file for scan %d', scanNr);
    return;
end
fid = fopen(fname);
fgetl(fid);
config = strsplit(fgetl(fid), ' ');

data=textscan(fid,repmat('%f ', 1, length(config)));

end