## Copyright (C) 2019 Ketan M. Patel
## 
## This program is free software: you can redistribute it and/or modify it
## under the terms of the GNU General Public License as published by
## the Free Software Foundation, either version 3 of the License, or
## (at your option) any later version.
## 
## This program is distributed in the hope that it will be useful, but
## WITHOUT ANY WARRANTY; without even the implied warranty of
## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
## GNU General Public License for more details.
##
## You should have received a copy of the GNU General Public License
## along with this program.  If not, see
## <https://www.gnu.org/licenses/>.

## -*- texinfo -*- 
## @deftypefn {} {@var{obj} =} fromJSON (@var{str},@var{sarray})
##
##  Converts any JSON string @var{str} into an applicable native Octave object:
##  number, logical, string, struct.  
##  'null' is converted to NaN and any numerical string >= +/-1e308 is converted
##  to +/-Inf.   Unrecognizable, unquoted JSON fragments are returned as Octave string.
##
##  Where possible, nested JSON array will be returned as matrix or ND array.
##  String arrays, mixed class and mixed size JSON arrays will returns as 
##  Octave cell arrays.
##
##  For JSON object with member arrays, if @var{sarray} is TRUE (default) and 
##  if possible, an Octave struct array is returned; otherwise, a scalar struct 
##  with member arrays is returned.   
##
##  Setting @var{sarray} to TRUE is recommended for parsing JSON strings with
##  structured numerical data, while setting to FALSE is recommeded for parsing 
##  miscellaneous, mixed-class information.
##
##  Complex number in JSON object format i.e. {"re":..., "im": ...} or 
##  {"real":..., "imag":...} will be converted a complex number matrix, 
##  provided that both "re" and "im" members are present and of equal size.
##
## @seealso{toJSON}
## @end deftypefn

## Author: Ketan M. Patel <kmpatel@roc-photonics.com>
## Created: 2019-06-01
## Updates:
## 2020-10-22 Refactored and documented (Ketan M. Patel)
## 2020-10-27 Updated from Octave 7 compatibility

function [obj] = fromJSON (str='', SARRAY=[]);

  if !( isempty(SARRAY) || isbool(SARRAY) );
    _warn_("Invalid SARRAY");
    SARRAY = [];
  endif
  
  % set defaults
  ischar(str)      || (str='');
  !isempty(SARRAY) || (SARRAY=true); 
  
  str = regexprep(str,'^[\s,]*','');  
  
  wstate = warning('off','Octave:num-to-str');
  
  if( isempty(str) );  
    obj = [];
  else
    obj = _fromjson_(str,SARRAY);
  endif

  warning(wstate);
  
endfunction

function [obj] = _fromjson_(str,SARRAY);
  
    obj = {};
    while( str );
      [obj{end+1} str] = _str2obj_(str,SARRAY);
    endwhile
    
    if( numel(obj) == 1 );
      obj = obj{1};
    elseif( !isstruct(obj{1}) || SARRAY );
      obj = _ndarray_(obj);
    endif
    
endfunction

function [obj,remain] = _str2obj_(str,SARRAY);  

    [btype, block, remain] = _get_block_(str);
    remain = regexprep(remain,'^[\s,]*','');     % keep this HERE, not in _get_block_
    
    switch( btype );
      case '[';       obj = _to_array_  (block,SARRAY);
      case '{';       obj = _to_struct_ (block,SARRAY);
      case {'"',"'"}; obj = _try_to_fn_ (block);
      
      otherwise;  eval(['obj=[' _rep_(block, Inf) '];'], ...
                       ['_warn_("Invalid frag", (obj=block));']);
    endswitch
    
endfunction  

function [btype, block, remain] = _get_block_ (str);

    switch( btype=str(1) );
      case '[';   idx = [strfind(str,'[') -strfind(str,']')];
      case '{';   idx = [strfind(str,'{') -strfind(str,'}')];
      case {'"',"'"};
                  idx = regexp(str(2:end),['(?<!\\)' btype], 'once');
                  idx = [1 -(idx+1)];

      otherwise;  [block,remain] = strtok(str,',');  
                  return;
    endswitch
    
    % exclude escaped-brackets (e.g.'\[', '\{', '\"', etc)
    tmp = abs(idx); tmp(2:end)--;
    idx(str(tmp) == '\') = [];   

    if( numel(idx) == 1 );                  % unterminated block
      close = numel(str)+1;
    elseif( idx(2) < 0 );                   % non-nested block
      close = -idx(2);
    else                                    % nested block
      idx   = idx([~,i] = sort(abs(idx)));  % sort by abs(idx)
      count = cumsum(sign(idx));        
      close = -idx(find(count==0, 1));
      close || (close = numel(str)+1);     % if necessary, autoclose at end of str
    end
    
    close <= numel(str) || _warn_("Unclosed block", str(1:min(20,end)));
    
    block  = str(2 : close-1);
    remain = str(close+1 : end);
 
endfunction

%========================= str2... =======================

function [mat] = _to_array_(str,SARRAY);
  
    % look for >2D array or assignment (unsafe)  
    ok = !( regexp(str,'^\s*\[\s*\[') || strfind(str,'=') );  % <== faster done seperately

    if( ok );
      mat = regexprep(_rep_(str, Inf), ',(?=\s*\[)',';');

      if( regexp(mat,'^[\s[]*{' ) );    % first array element is structure
        mat = _rep_(mat, struct);
        eval([_lazyJSON_ 'mat=_specials_([' mat ']); ok=true;'], 'ok=false;');
      else
        eval(['mat=[' mat ']; ok=!ischar(mat) && !iscell(mat);'],'ok=false;');
      endif
    endif

    if( !ok );              % this can be very slow, avoid getting here if possible
      mat = _fromjson_(str,SARRAY);
    elseif isstruct(mat);   % double-check struct array for special members
      mat = structfun(@_specials_,mat,'uniformoutput',false);
    endif
   
endfunction 

function [obj] = _to_struct_(str,SARRAY);

    frag = str(1:min(end,20)); % keep frag for warning msg
    vals = keys = {};
    
    try; while( str );       % may fail for incomplete JSON struct string
      [keys{end+1} str] = regexp(str,'^\s*"?(.*?)"?\s*:\s*(.*)$','tokens','once'){:}; 
      [vals{end+1} str] = _str2obj_(str,SARRAY);
    endwhile; catch;
      _warn_("Malformed struct frag", ['{' frag '...}']);
    end_try_catch

    try;
      SARRAY || isempty(vals) || error("don't");
      obj = struct([keys; _num2cell_(vals)]{:});   % may fail for mixed-size vals
    catch;
      for i = 1:numel(vals);  
        obj.(keys{i}) = vals{i};            
      endfor;
    end_try_catch
    
    obj = _specials_(obj);
    
endfunction

function [fn] = _try_to_fn_(fn);
    try 
      regexp(fn,'^\s*@') && (fn = str2func(fn));
    catch
      _warn_("Invalid inline function", fn);
    end;
endfunction

%========================= helpers =======================

function str = _rep_(str, arg);

  if isstruct(arg);
    arg = {'{','struct(',':',',','}',')'};
  elseif isinf(arg)
    arg = {'null','NaN ','e308','*Inf'};
  endif
  
  do
    str = strrep(str,arg{1:2},'overlaps',false);
    arg(1:2) = [];
  until isempty(arg);
  
endfunction

function [c] = _num2cell_(c);
  for i=1:numel(c);
    if( ischar( _c_=c{i} ) || isempty(_c_) );
      c{i} = {_c_};
    elseif( !iscell(_c_) );
      c{i} = num2cell(_c_);
    endif
  endfor
endfunction

function [c] = _ndarray_(c);  % try to make ND array (either mat or cell)

  sz = size(e = c{1}); t = class(e);
  
  if !( !ischar(e) && sz && cellfun(@(v) isa(v,t) && size(v)==sz, c) );
    return;
  elseif( isscalar(e) );     % try matrix conversion, ok to fail if not uniform class
    try c = [c{:}]; end;   
  elseif( isvector(e) );     % cell array of mat or cell array
    c = reshape([c{:}], numel(e), numel(c)).';
  else                    % make ND array
    index.type = "()";
    index.subs = repmat({':'},1,ndims(e)+1);
    i = numel(c);
    
    do
      index.subs{end} = i;
      e = subsasgn(e,index,c{i});
    until (--i) == 1;
    c = e;
    
  endif
    
endfunction

function [s] = _specials_(s);   % convert special structures

  if( !isstruct(s) ); return; endif;
  
  keys = __fieldnames__(s);
  
  if( ismember(keys,{'real','imag'}) ); _to_complex_([s.real],[s.imag]);
  elseif( ismember(keys,{'re','im'}) ); _to_complex_([s.re  ],[s.im  ]);
  endif

  function _to_complex_(re,im);
    if( isscalar(s) && size_equal(re,im) );
      s = complex(re,im);
    elseif( prod( sz=size(s) ) == numel(re) );
      s = reshape(complex(re,im),sz);
    endif
  endfunction
  
endfunction

% for max conversion speeed, gaurd against lazy JSON (unquoted keys) of common structures
function [str] = _lazyJSON_();
  str = 'x="x";y="y";z="z";real=re="re";imag=im="im";r="r";rho="rho";theta="theta";';
endfunction

function out = _warn_(msg,frag='');
  frag = strtrim(frag);
  out = warning([msg ifelse(isempty(frag),'',[': "' frag '"']) "\n"]);
endfunction

%!test  ## input validation
%! assert(fromJSON(),[]); % ok, reference
%!warning <Invalid SARRAY> fromJSON([],struct);
%!
%! bad = {4,{},@sin,struct(),'',false,0,[1,2,3]};
%! assert(all(cellfun(@(i)isempty(fromJSON(i)),bad)));  % all bad, return []

%!test  ## number
%! assert(fromJSON('4'),4)

%!test  ## number string
%! assert(fromJSON('"string"'),"string")

%!test  ## bool
%! assert(fromJSON('true'),true)

%!test  ## numerical array
%! assert(fromJSON('[1,2,3,4]'),1:4)
%! assert(fromJSON('[[1,2],[3,4]]'),[1 2;3 4])

%!test  ## infinity
%! assert(fromJSON('1e308'),Inf)
%! assert(fromJSON('1e999'),Inf)
%! assert(fromJSON('Inf'),  Inf)
%! assert(fromJSON('[1,1e308,-1e308]'),[1 Inf -Inf])

%!test  ## null
%! assert(fromJSON( 'null' ),NaN)
%! assert(fromJSON('"null"'),"null")

%!test  ## empty array
%! assert(fromJSON('[]'),[]);

%!test  ## numerical matrix
%! assert(fromJSON('[[1,2],[3,4]]'),[1 2;3 4])

%!test  ## bool matrix
%! assert(fromJSON('[[true,false];[false,true]]'),!![1 0;0 1])

%!test  ## numeric/bool matrix
%! assert(fromJSON('[[true,3];[false,true]]'),[1 3;0 1])

%!test  ## numerical ND array
%! assert(fromJSON('[[[1,3,5],[2,4,6]],[[7,9,11],[8,10,12]]]'),reshape(1:12,2,3,2));

%!test  ## more N numerical ND array
%! json = "[[[[[1,3],[2,4]],[[5,7],[6,8]]],[[[11,13],[12,14]],[[15,17],[16,18]]]],[[[[21,23],[22,24]],[[25,27],[26,28]]],[[[31,33],[32,34]],[[35,37],[36,38]]]]]";
%! assert(fromJSON(json),reshape([1:8 11:18 21:28 31:38],2,2,2,2,2));

%!test  ## cell array
%! assert(fromJSON('[["a",2,3],[4,"b",5]]'), {'a' 2 3; 4 'b' 5});

%!test  ## mismatch nested array
%! assert(fromJSON('[[1,2,3,4,5],[1,2]]'),{[1 2 3 4 5] [1 2]})

%!test  ## more mismatched nested array
%! assert(fromJSON('[1,2,3,[2,3]]'),{1,2,3,[2,3]})

%!test  ## mixed class array
%! assert(fromJSON('[[1,2,3,"a"],[2,3,4,4]]'),{{1 2 3 "a"},[2 3 4 4]})

%!test  ## more N numerical ND array
%! json = "[[[[[1,3],[2,4]],[[5,7],[6,8]]],[[[11,13],[12,14]],[[15,17],[16,18]]]],[[[[21,23],[22,24]],[[25,27],[26,28]]],[[[31,33],[32,34]],[[35,37],[36,38]]]]]";
%! json = regexprep(json,'(\d+)','"$1"'); % turn it input JSON array of strings
%! c    = cellfun(@num2str,num2cell(reshape([1:8 11:18 21:28 31:38],2,2,2,2,2)), 'uniformoutput', false);
%! assert(fromJSON(json),c);

%!test  ## JSON-like, with Octave numerical notation (bonus feature)
%! assert(fromJSON('[Inf,-Inf,NaN,2i,pi,e]'),[Inf,-Inf,NaN,2*i,pi,e],1e-15);

%!test   ## beautified JSON
%! obj=fromJSON("\n[\n\t  [1,2,3,4],\n\t  [2,3,4,4]\n]    ");
%! assert(obj,[[1 2 3 4];[2 3 4 4]])

%!test   ##  incomplete array
%! assert(fromJSON("[1,2,3  "),[1,2,3]);

%!test   ##  more incomplete array
%! assert(fromJSON("[[1,2,3],[3"),{[1,2,3],[3]});

%!test  ## string with whitespaces
%! assert(fromJSON("\"te\nss      df\t t\""),"te\nss      df\t t")

%!test  ## char array
%! assert(fromJSON('["a","b","c"]'),{'a','b','c'})

%!test  ## array of string
%! assert(fromJSON('["test","list","more"]'),{'test',"list","more"})

%!test  ## escaped quote
%! assert(fromJSON('["te\"t","list","more"]'),{'te\"t' 'list' 'more'})

%!test  ## struct
%! assert(fromJSON('{}',true), struct())
%! assert(fromJSON('{}',false),struct())

%!test  ## struct
%! assert(fromJSON('{"a":3,"b":5}',true),struct("a",3,"b",5))

%!test  ## lazy JSON struct
%! assert(fromJSON('{a:3,b:5}',true), struct("a",3,"b",5))

%!test   ## beautified object notation
%! assert(fromJSON("{\n\ta\t  :\t  3\n\t}"), struct("a",3))

%!test ## struct of vector
%! assert(fromJSON('{a:[1,2,3,4]}',            true), struct('a',{1 2 3 4}));
%! assert(fromJSON('[{a:1},{a:2},{a:3},{a:4}]',true), struct('a',{1 2 3 4}));
%! assert(fromJSON('{a:[1,2,3,4]}',           false), struct('a',[1 2 3 4]));

%!test ## struct of 2x2 array
%! assert(fromJSON('{a:[[1,3],[2,4]]}',          true), struct('a',{1 3;2 4}));
%! assert(fromJSON('[{a:1},{a:2}],[{a:3},{a:4}]',true), struct('a',{1 2;3 4}));
%! assert(fromJSON('{a:[[1,3],[2,4]]}',         false), struct('a',[1 3;2 4]));

%!test ## array of struct with SARRAY=false
%! assert(fromJSON('[{a:1},{a:2} , {a:3},{a:4}]',false), num2cell(struct('a',{1 2 3 4})));
%! assert(fromJSON('[{a:1},{a:2}],[{a:3},{a:4}]',false), num2cell(struct('a',{1 2;3 4})));

%!test  ## struct with mixed-size arrays (will not honor SARRAY=true)
%! assert(fromJSON('{"a":[1,2],"b":[3,4,5]}',true), struct("a",[1,2],"b",[3,4,5])) 

%!test  ## struct with number and string (guard against turing string into char array)
%! assert(fromJSON('{"a":1,"b":"hello"}',true), struct("a",1,"b","hello")) 

%!test  ## struct with empty array  (gaurd against returning empty struct array)
%! assert(fromJSON('{"a":3,"b":[]}',true), struct("a",3,"b",[])) 

%!test  ## incomplete struct
%! assert(fromJSON('[[1,2,{a:3,b ],3,4,5]'), {{1,2,struct('a',3)},3,4,5});

%!test  ## **nested** struct with array
%! assert(fromJSON('{"a":{"b":[1,2,3]}}',true ), struct("a",num2cell(struct("b",{1,2,3}))))  % <== struct array
%! assert(fromJSON('{"a":{"b":[1,2,3]}}',false), struct("a",struct("b",[1,2,3])))            % <== struct with array b

%!test  ## struct with mixed class array 
%! assert(fromJSON('{"b":[1,2,{c:4}]}',true),struct('b',{1,2,struct('c',4)}));
%! s.b = {1,2,struct("c",4)};
%! assert(fromJSON('{"b":[1,2,{c:4}]}',false),s)

%!test   ## key with spaces
%! obj=fromJSON('{key with space: 4,"a":3}');
%! assert(obj,struct("a",3,"key with space",4))

%!test   ## duplicate key
%! assert(fromJSON('{a:3,"a":5}'),struct("a",5))

%!test   ## empty object key-val
%! obj=fromJSON('{a:3,,,    ,     "b"   :5}');     
%! assert(obj,struct("a",3,"b",5))

%!test ## 2x2 array of struct 
%! obj=fromJSON('[[{a:1},{a:3}],[{a:2},{a:4}]]');
%! assert(obj,struct('a',{1 3;2 4}));

%!test ## 2x2 array of struct of arrays NOTE: this is sketchy operation
%! obj=fromJSON('[[{"a":[1,1]},{"a":[3,3]}],[{"a":[2,2]},{"a":[4,4]}]]');
%! assert(obj,struct('a',{[1 1] [3 3];[2 2] [4 4]}));

%!test ## struct of ND array
%! obj=fromJSON('{a:[[[1,3],[2,4]],[[11,13],[12,14]]]');
%! assert(obj,struct('a',num2cell(reshape([1:4 11:14],2,2,2))));

%!test ## ND array of struct 
%! obj=fromJSON('[[[{a:1},{a:3}],[{a:2},{a:4}]],[[{a:11},{a:13}],[{a:12},{a:14}]]]');
%! assert(obj,struct('a',num2cell(reshape([1:4 11:14],2,2,2))));

%!test  ## mixed array with struct
%! assert(fromJSON('[2,{a:3,"b":5}]'),{2,struct("a",3,"b",5)})

%!test ## more mixed array with struct
%! assert(fromJSON('[{a:3,"b":5},{a:3}]'),{struct("a",3,"b",5),struct("a",3)})

%!test  ## complex number struct
%! assert(fromJSON('{re:3,im:5}'),    3+5i);
%! assert(fromJSON('[{re:3,im:5}]'),  3+5i);
%! assert(fromJSON('{real:4,imag:6}'),4+6i);

%!test  ## complex number struct with anomoly
%! assert(fromJSON('{im:3,re:5}'),           5+3i);
%! assert(fromJSON('{im:3,re:5,re:7,im:10}'),7+10i);

%!test  ## complex number struct array
%! assert(fromJSON('{re:[4,5],im:[6,7]}'),          [4+6i,5+7i]     );
%! assert(fromJSON('[{re:[4,5],im:[6,7]}]'),        [4+6i,5+7i]     );
%! assert(fromJSON('[{re:[4,5;0 2],im:[6,7;3 0]}]'),[4+6i,5+7i;3i 2]);

%!test  ## complex number struct in array
%! obj=fromJSON('[[{re:4,im:6},{re:1,im:0}],[{re:0,im:2}, {re:2,im:4]]');
%! assert(obj,[4+6i 1;2i 2+4i]);

%!test  ## complex number struct mixed with number within array
%! obj=fromJSON('[[4,{re:1,im:0}],[{re:0,im:2}, {re:4,im:6}]]');
%! assert(obj,[4 1;2i 4+6i]);

%!test  ## complex number struct within a structure (nested)
%! assert(fromJSON('{"a": {re:1,im:5}}'),struct('a',1+5i));

%!test  ## complex number struct with structure array (nested)
%! obj = fromJSON('[{"a":{re:1,im:5}},{"a":{re:2,im:5}}]');
%! assert(obj,struct('a',{1+5i,2+5i}));

%!test  ## test apparent octave inline fn  (convert to inline)
%! assert(fromJSON('"@sin"'),@sin);
%! assert(func2str(fromJSON('"@(x)3*x"')),func2str(@(x)3*x));

%!test   ## exotic object in structure
%! assert(fromJSON('{"a":"[java.math.BigDecimal]"}'),struct('a','[java.math.BigDecimal]'));

%!test  ## JSON with confusing '[]{},' or missing quotes (hard string parse test)
%! obj=fromJSON('[{a:"tes, : [ ] t"},"lkj{} sdf",im mi{}ing quotes]');
%! assert(obj,{struct('a',"tes, : [ ] t"), 'lkj{} sdf','im mi{}ing quotes'})

%!test  ## garbage (non-quoted, meaningless string)
%! assert(fromJSON('garbage'),'garbage')

%!test  ## garbage in array
%! assert(fromJSON('[1,garbage]'),{1,'garbage'})

%!test  ## garbage in struct
%! assert(fromJSON('{a:garbage}'),struct('a','garbage'))

%!test   ## exotic object (placeholder of class name)
%! assert(fromJSON('"[java.math.BigDecimal]"'),'[java.math.BigDecimal]');

%!test   ##  warnings
%!warning <Invalid frag>   fromJSON('garbage');
%!warning <Malformed>      fromJSON('{a:3,b}');
%!warning <Unclosed>       fromJSON('{a:3,b:4');
%!warning <Invalid frag>   fromJSON('@nofunc');


