function success = ChangeXPPodeFile(filename,parset)
%ChangeXPPodeFile  Change parameters / initial conditions in XPP ODE file
%  (c) Robert Clewley, August 2004, 2007
%
%  Usage: success = ChangeXPPodeFile(filename,parset) changes the parameters and initial
%  conditions named in the parset structure to new values by directly modifying the named
%  ODE file. The return value success is a boolean.
%
%  IMPORTANT: All initial conditions to be changed must be explicitly declared in the ODE file
%  using 'init' or 'i', not using the format 'x(0)' for a variable x, etc. Also, implicitly-declared
%  zero initial conditions cannot be altered by this function. For these, you might find it better
%  to use the ChangeXPPsetFile function instead, with the setfile option of RunXPP.
%
%  'number' declarations in XPP are also not supported as range parameters.
%
%  Input arguments:
%  filename should end with the extension '.ode'
%  parset is a structure array with fields 'type', 'name', and 'val'. type must be one
%   of the strings 'PAR' or 'IC'.

success = false;

if isempty(filename) || exist(filename,'file') ~= 2
    error(' Problem with filename passed: file does not exist in specified path')
else
    if ~strcmp('.ode',filename(length(filename)-3:length(filename)))
        error(' Problem with filename passed: must end in `.ode`')
    end
end

cstr = computer;
if strcmp(cstr, 'MAC')
    compix = 0;
elseif strcmp(cstr, 'PCWIN')
    compix = 1;
else % assume UNIX etc. (no-one uses VMS any more)
    compix = 2;
end

fid = fopen(filename,'r');
file_done = false;
pars_found = false;
lineCount = 0;
odefile = {};
lenpars = length(parset);
parnames = cell(1,lenpars);
parnames = {parset(1:lenpars).name}; % make a simple cell array list of parnames to be changed
foundOccurrence = zeros(1,lenpars);

fieldsexpected = {'name','val','type'};
for ix=1:lenpars
    if ~isempty(setdiff(fieldnames(parset(ix)),fieldsexpected))
        fclose(fid);
        error([' Incorrect fields found in argument parset for entry ' num2str(ix)])
    end
	if ~ismember(parset(ix).type,{'PAR','IC'})
        fclose(fid);
        error([' Invalid type, ' parset(ix).type ', specified in entry ' num2str(ix)])
	end
end

% change specified pars when encountered
parsDone = false;
while ~parsDone
    fline = fgetl(fid);
    if ~ischar(fline) % then premature EOF
        fclose(fid);
        error(' End of file reached before specified params found. Cannot continue')
    end
    [token rest] = strtok(fline);
    isParType = strcmp(token, 'par') | strcmp(token, 'p');
    isICType = strcmp(token, 'init') | strcmp(token, 'i');
    if isParType || isICType
        [parsed numparsed] = ParseParLine(rest);
        if numparsed == 0 || isnan(parsed(numparsed).num)
            fclose(fid);
            error(' Problem parsing numerical value of parameter / initial condition in ODE file. Cannot continue')
        end
        if isParType
            flineNew = ['par '];
            typeStr = 'par';
        else
            flineNew = ['init '];
            typeStr = 'ic';
        end
        for i=1:numparsed
            [ispres ix] = ismember(parsed(i).name, parnames);
            if ispres % then a par to be changed has been found
                if (strcmp(parset(ix).type,'PAR') && ~isParType) || (strcmp(parset(ix).type,'IC') && isParType)
                    fclose(fid);
                    error([' Found ' typeStr '`' parset(ix).name '` that didn`t correspond to specified type ' parset(ix).type])
                end
                foundOccurrence(ix) = 1;
                if numparsed==1 || i==numparsed
                    flineNew = [ flineNew, parsed(i).name '=' num2str(parset(ix).val)  ];
                else
                    flineNew = [ flineNew, parsed(i).name '=' num2str(parset(ix).val) ', ' ];
                end
            else
                if numparsed==1 || i==numparsed
                    flineNew = [ flineNew, parsed(i).name '=' num2str(parsed(i).num)  ];
                else
                    flineNew = [ flineNew, parsed(i).name '=' num2str(parsed(i).num) ',' ];
                end
            end
        end
    else
        flineNew = fline;
    end
    odefile = {odefile{:}, flineNew};
    lineCount = lineCount + 1;
    if sum(foundOccurrence) == lenpars % then made all changes necessary
        parsDone = true;
    end
end

% pull in rest of file once params changed
while ~file_done
    fline = fgetl(fid);
    if ~ischar(fline) % then EOF
        file_done = true;
    else
        odefile = {odefile{:}, fline};
        lineCount = lineCount + 1;
    end
end
fclose(fid);

if compix == 1
	% windows uses \r\n line termination
	lineterm = '\r\n';
else
	lineterm = '\n';
end

fidw = fopen( filename ,'w');
if fidw ~= -1
	for line=1:lineCount
        fprintf(fidw, ['%s' lineterm], odefile{line});
	end
	fclose(fidw);
else
    error([' Problem opening file ' filename ' for writing.']);
end
success = true;
return


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%


function [parsed, numparsed] = ParseParLine(full_line)
parseddef.num = NaN;
parseddef.name = '';
fline = full_line(isspace(full_line)==0); % get rid of whitespace
fline = strrep(fline,'=',' '); % convert = to space
fline = strrep(fline,',',' '); % convert , to space
numparsed = 0;
endofline = false;
while ~endofline
    [nameStr restStr] = strtok(fline);
    if isempty(nameStr)
        endofline = true;
        continue
    end
    numparsed = numparsed + 1;
    [numStr restStr] = strtok(restStr);
    if isempty(numStr) || isempty(nameStr)
        parsed = parseddef;
        error('Param parse error: Parameter name or value missing!')
    end
    if ~isNum(numStr(isspace(numStr)==0))
        parsed = parseddef;
        error('Param parse error: Parameter value not a number!')
    end
    parsed(numparsed).name = nameStr(isspace(nameStr)==0); % get rid of all whitespace
    parsed(numparsed).num  = str2num(numStr);
    fline = restStr;
end
return


%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

function result = isNum(data)
% parameter `data` is a string
result = false;
pointCount = 0;
num = [char(48:57),'.','-','e'];
for i=1:length(data)
    if data(i) == '.'
        pointCount = pointCount + 1;
        if pointCount > 1
            return % false
        end
    end
    if ~ismember(data(i),num)
        return % false
    end
end

result = true; % can only get here if all chars were numeric
return