function model = PropagateNamespaces(model,map)
%% model = PropagateNamespaces(model,name_map)
% purpose: namespace-establishing namespace substitutions.
% inputs:
%   model - DynaSim model structure (see GenerateModel)
%   name_map - cell matrix mapping parameter, variable, and function names
%   between the user-created specification (population equations and mechanism
%   files) and the full model with automatically generated namespaces. It 
%   has four columns with: user-specified name, name with namespace prefix, 
%   namespace, and type ('parameters', 'fixed_variables', 'state_variables', 
%   'functions', or 'monitors').
% outputs:
%   model - DynaSim model structure with namespace added as namespace-delineating prefix
%
% Example 1: ...
% 
% see also: GenerateModel, PropagateFunctions, ParseModelEquations, GetParentNamespace

% Check model
model=CheckModel(model);
% Check map
if ~iscell(map) || size(map,2)~=4
  error('map must be a cell array with four columns for (name, namespace_name, namespace, type)');
end
names_in_namespace=cellfun(@(x,y)strncmp(y,x,length(y)),map(:,2),map(:,3)); 

% namespace propagation pattern:
allowed_insert_types.fixed_variables=...
  {'parameters','fixed_variables','reserved'}; % into fixed_variables
allowed_insert_types.functions=...
  {'parameters','fixed_variables','functions','state_variables','reserved'}; % into functions
allowed_insert_types.monitors=...
  {'parameters','fixed_variables','functions','state_variables','reserved'}; % into monitors
allowed_insert_types.ODEs=...
  {'parameters','fixed_variables','functions','state_variables','reserved'}; % into ODEs
allowed_insert_types.ICs=...
  {'parameters','fixed_variables','functions','reserved'}; % into ICs
allowed_insert_types.linkers=...
  {'parameters','fixed_variables','functions','state_variables','reserved'};
allowed_insert_types.conditionals=...
  {'parameters','fixed_variables','functions','state_variables','reserved'};

%% 1.0 Propagate through structure arrays (linkers, conditionals)
% 1.1 linkers (expression)
if ~isempty(model.linkers)
  namespaces={model.linkers.namespace};
  expressions=propagate_namespaces({model.linkers.expression},namespaces,map,allowed_insert_types.linkers);
  [model.linkers(1:length(model.linkers)).expression]=deal(expressions{:});
end

% 1.2 conditionals (condition, action, else)
if ~isempty(model.conditionals)
  namespaces={model.conditionals.namespace};
  target_types={'condition','action','else'};
  for type_index=1:length(target_types)
    type=target_types{type_index};
    tmp=propagate_namespaces({model.conditionals.(type)},namespaces,map,allowed_insert_types.conditionals);
    [model.conditionals(1:length(model.conditionals)).(type)]=deal(tmp{:});
  end
end

%% 2.0 Propagate through sub-structures
target_types={'fixed_variables','functions','monitors','ODEs','ICs'};

% loop over types of model data
for type_index=1:length(target_types)
  type=target_types{type_index};%'fixed_variables';
  % info for this type
  s=model.(type); 
  if isstruct(s)
    fields=fieldnames(s); % namespaced-names of this type (ie, [namespace_name])
    expressions=struct2cell(s); % raw-expressions of this type (ie, without namespace prefixes)
    namespaces={};
    for i=1:length(expressions)      
      idx=strcmp(fields{i},map(:,2));
      if numel(find(idx))>1
        % constrain to namespace-conserving entries
        tmp=map(idx&names_in_namespace,3);
        % use the lowest level namespace (i.e., longest namespace name)
        l=cellfun(@length,tmp);
        tmp=tmp{l==max(l)};
      else
        tmp=map{idx,3};
      end
      namespaces{end+1}=tmp;
    end
    % update expressions for names of this type
    expressions=propagate_namespaces(expressions,namespaces,map,allowed_insert_types.(type));
    % update model with expressions including namespaces
    model.(type)=cell2struct(expressions,fields,1);
  end
end

%% NESTED FUNCTIONS
% function expressions=propagate_namespaces(expressions,names_full,map,insert_types)
function expressions=propagate_namespaces(expressions,namespaces,map,insert_types)
  % loop over and update expressions for names of this type
  for i=1:length(expressions)    
    if isempty(expressions{i})
      continue;
    end
    % get namespace for this expression
    this_namespace=namespaces{i};
    % find parent namespaces
    parent_namespace = GetParentNamespace(this_namespace);
    % find where this and parent namespaces are in map array    
    insert_type_constraint = ismember(map(:,4),insert_types);
    this_namespace_map_inds = find(strcmp(this_namespace,map(:,3)) & insert_type_constraint);
    parent_namespace_map_inds = find(strcmp(parent_namespace,map(:,3)) & insert_type_constraint);
    % get list of words in this expression
    words=unique(regexp(expressions{i},'[a-zA-Z]+\w*','match'));
    % loop over words
    for j=1:length(words)
      % search for words in parent namespace of map.names
      if any(strcmp(words{j},map(parent_namespace_map_inds,1))) % search parent namespace
        % word found in parent namespace of map
        ind=parent_namespace_map_inds(strcmp(words{j},map(parent_namespace_map_inds,1)));      
        new_word=map{ind,2};
        % replace found word in expression by map(names_bar|parent_namespace)
        expressions{i}=dynasim_strrep(expressions{i},words{j},new_word);
        % check whether new word is defined in model
        % note: this is necessary to account for namespace differences between
        % user-supplied population parameters that should replace default mechanism-level parameters
        new_word_type=map{ind,4};
% %{        
        if ~isfield(model.(new_word_type),new_word)
          % if not, define it from (word without namespace/namespace)
          if isfield(model.(new_word_type),words{j})
            model.(new_word_type).(new_word)=model.(new_word_type).(words{j});
            model.(new_word_type) = rmfield(model.(new_word_type),words{j});
          else
            tmpi=find(strcmp(words{j},map(:,1))&strcmp(new_word_type,map(:,4))&strcmp(this_namespace,map(:,3)));
            if ~isempty(tmpi)
              old_field = map{tmpi,2};
              if ~isempty(tmpi) && isfield(model.(new_word_type),old_field)
                model.(new_word_type).(new_word)=model.(new_word_type).(old_field);              
                %model.(new_word_type) = rmfield(model.(new_word_type),old_field);
              end
            end
          end
        end
% %}
      elseif any(strcmp(words{j},map(this_namespace_map_inds,1))) % search this namespace
        % word found in this namespace of map
        ind=this_namespace_map_inds(strcmp(words{j},map(this_namespace_map_inds,1)));      
        new_word=map{ind,2};
        % replace found word in expression by map(names_bar|this_namespace)
        expressions{i}=dynasim_strrep(expressions{i},words{j},new_word);
      end
    end
  end
end

end