classdef check_headers < matlab.unittest.TestCase
  % Checks whether desired copyright headers are in place
  % Can be used to fix them automatically by setting `dofix=true`
  % mytest = check_headers
  % mytest.dofix = true; % changes property
  % mytest.run; % runs
  % Concrete sub-classes must redefine the abstract properties below
  % They can also extend the check_and_fix_files method to add the
  % TestTags attribute if needed.
  %
  % [+GenLib General Purpose Library+] Swiss Plasma Center EPFL Lausanne 2022. All rights reserved.
  
  properties (Abstract)
    dofix               % if true, modify files to fix header
    IgnoredPaths        % cell array of paths to ignore
    old_headers         % cell array of old headers to remove
    desired_header      % array of char with new desired header
  end

  methods (TestClassSetup)
    function check_MATLAB_version(testCase)
      testCase.assumeTrue(~verLessThan('matlab','9.2'),'check_headers tests are disabled for version 2016b and prior');
    end
  end

  methods(Test)
    
    function check_and_fix_files(testCase)
      %% .m, .c and .h files
      mfiles = dir(fullfile(testCase.test_folder,'**/*.m'));
      cfiles = dir(fullfile(testCase.test_folder,'**/*.c'));
      hfiles = dir(fullfile(testCase.test_folder,'**/*.h'));
      files = [mfiles;cfiles;hfiles];
      
      % remove ignored paths
      files = files(~contains({files.folder},testCase.IgnoredPaths));
      if testCase.dofix && numel(files)>0
        s = input(sprintf('this will check and fix %d files in %s/, are you sure? (y/n)',...
          numel(files),testCase.test_folder),'s');
        if ~contains(lower(s),'y'), return; end
      end
      
      failing_files = {}; % init
      for ifile = 1:numel(files)
        myfile = files(ifile);
        filepath = fullfile(myfile.folder,myfile.name);
        
        fprintf('checking file: %s \n',filepath)
        
        filewhole = fileread(filepath); % full listing
        if contains(filewhole,testCase.desired_header)
          continue; % next file
        end
        
        % check if old header text exists and remove it
        if  any(contains(filewhole,testCase.old_headers))
          remove_unwanted_headers(filepath,testCase.old_headers,testCase.dofix);
        end
        
        if testCase.dofix
          % Add new help header
          add_header(filepath,testCase.desired_header)
        end
        failing_files = [failing_files;{filepath}]; %#ok<AGROW>
      end % loop on files
      if ~testCase.dofix && ~isempty(failing_files)
        errormsg = sprintf('These files did not contain the desired header: %s\n',failing_files{:});
        testCase.assertFail(errormsg);
      end
    end
  end
end

function remove_unwanted_headers(filename,old_headers,dofix)
% remove unwanted headers if any
for iunwanted = 1:numel(old_headers)
  my_unwanted_header = old_headers{iunwanted};
  grepcmd = sprintf('grep -n "%s" %s | cut -f1 -d:',my_unwanted_header,filename);
  [s,w] = system(grepcmd);
  assert(s==0,'error using grep: %s\n',w);
  if ~isempty(w)
    fprintf('found unwanted header on line %d\n',str2double(w))
    if dofix
      % replace special characters with escape char
      my_unwanted_header_regexp = my_unwanted_header;
      my_unwanted_header_regexp = strrep(my_unwanted_header_regexp,'+','\+');
      my_unwanted_header_regexp = strrep(my_unwanted_header_regexp,'[','\[');
      my_unwanted_header_regexp = strrep(my_unwanted_header_regexp,']','\]');
      % remove unwanted header using sed
      fprintf(' removing unwanted header\n')
      sedcmd = sprintf('sed -i '''' ''/%s/d'' %s',my_unwanted_header_regexp,filename);
      [s,w] = system(sedcmd);
      assert(s==0,'error running sed: %s\n',w);
    end
  end
end
end
function add_header(filename,desired_header)

[~,~,ext] = fileparts(filename);
if strcmp(ext,'.m')
  desired_header = ['% ' ,desired_header]; % add comment character
  add_matlab_help_header(filename,desired_header);
else
  desired_header = ['/* ',desired_header,' */']; % add comment syntax
  add_top_header(filename,desired_header)
end
end

function add_top_header(filename,desired_header)
% add header at beginning of file
fprintf('adding header at the top of the file')
sedcmd = sprintf('sed -i '''' "1i \\\\\n%s\n" %s',desired_header,filename);
[s,w] = system(sedcmd);
assert(s==0,'error using sed.\nCommand: \n%s\nError:\n%s',sedcmd,w);
fprintf('\n');
end

function add_matlab_help_header(filepath,desired_header)
%% add desired header at the end of the help section
% find last help line
helptext = help(filepath);
[~,fname] = fileparts(filepath);
if contains(helptext,{sprintf('%s is a class',fname),sprintf('%s is a script',fname)})
  % default help text means the file has no help
  warning('%s has no help section, could not add header',filepath);
  return
end

% find end of help section by looking for consecutive lines that start with %
grepcmd = sprintf('grep -n ''^\\s*%%'' %s  | cut -f1 -d:',filepath);
[s,w] = system(grepcmd); assert(s==0,'error using grep:\n%s',w);
commentlines = str2num(w);  %#ok<ST2NM> % lines with comments
if isempty(commentlines), warning('no comment lines, can not fix %s',filepath); return; end
n_consecutive = find(diff(commentlines)~=1,1,'first'); 
if isempty(n_consecutive), n_consecutive = numel(commentlines); end % case they are all consecutive
n_last_helpline = commentlines(1)+n_consecutive-1; % consecutive comment lines

% check which lines contain only a comment character
[s,w] = system(sprintf('head -n%d %s | tail -n1',n_last_helpline,filepath));
assert(s==0,'error using head or tail:\n%s',w);
lastline = w;
add_emptycommentline = ~strcmp(strip(lastline),'%'); % add empty comment line if last help line is not just an empty character
indentation = find(lastline=='%',1,'first')-1;

fprintf('adding header at the end of the help section')

n_line_to_insert = n_last_helpline+1;

% total number of lines
[s,w] = system(sprintf('wc -l < %s',filepath)); assert(s==0,'error using wc: \n %s',w);
n_lines = str2double(w);
end_of_file = (n_line_to_insert>n_lines);

% if not already present, add one empty comment line before the header
if add_emptycommentline
  if ~end_of_file % insert
    sedcmd = sprintf('sed -i '''' "%di \\\\\n%*s%%\n" %s',n_line_to_insert,indentation,'',filepath);
  else % add after
    sedcmd = sprintf('sed -i '''' "%da \\\\\n%*s%%\n" %s',n_line_to_insert-1,indentation,'',filepath);
  end
  [s,w] = system(sedcmd);
  assert(s==0,'error using sed.\nCommand: \n%s\nError:\n%s',sedcmd,w);
  n_line_to_insert = n_line_to_insert+1;
end

% add the header
if ~end_of_file % insert before
  sedcmd = sprintf('sed -i '''' "%di \\\\\n%*s%s\n" %s',n_line_to_insert,indentation,'',desired_header,filepath);
else % add after
  sedcmd = sprintf('sed -i '''' "%da \\\\\n%*s%s\n" %s',n_line_to_insert-1,indentation,'',desired_header,filepath);
end

[s,w] = system(sedcmd);
assert(s==0,'error using sed.\nCommand: \n%s\nError:\n%s',sedcmd,w);
fprintf('\n');
end

