## Copyright (C) 2020 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})
##
##  Converts any JSON string @var{str} into an Octave object.  All numerical 
##  values other than char and logical are converted to double numerical class.  
##
##  'null' is  converted to NaN and any numerical string graater than or equal
##  to +/-1e308 (in magnitude) is converted to +/-Inf.
##
##  Where possible, char or numerical arrays will be converted to matricies (2D). 
##  Arrays of higher dimension are converted to cell array of matricies.  
##  Array of mixed-size arrays or arrays of mixed-classes are returned as cell array.
##
##  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.
##
##  Either an array of JSON object or JSON object of arrays, i.e 
##
##       '[[{"re":...,"im":...},{"re"...,"im":...},...]]' 
##  or   '{"re": [[....]], "im" :[[...]]}' 
##
##  will be converted equally, but converting the latter is significatly faster.
## 
##  fromJSON will also handle lazy JSON notations and non-standard numbers such
##  as pi, e, Inf, etc. e.g.
##      '{a:pi}'  => struct(a,3.1415...)  
##
## @seealso{toJSON}
## @end deftypefn

function [val] = fromJSON (str='');

  if str;
    [val str] = _parse_str_(str);
  else
    val = [];
  endif
  
  if str;     % more str!?  lazy JSON
    val = {val};
    while str;
      [val{end+1} str] = _parse_str_(str);
    endwhile
  endif
  
endfunction


function [val,remain] = _parse_str_(str);  

  [btype, block, remain] = _get_block_(str);
  
  switch btype;
    case {'"',"'"}; val = _to_string_(block);
    case '[';       val = _to_array_ (block);
    case '{';       val = _to_struct_(block);
    otherwise;      val = _to_value_ (block);
  endswitch

endfunction  


function [btype, block, remain] = _get_block_ (str);
  
    str = strtrim(str);
    
    sfind = @(c) strfind(str,c);
    btype = str(1);
    
    switch btype;

      case '"';   idx = sfind('"'); idx(2:2:end) *= -1;
      case "'";   idx = sfind("'"); idx(2:2:end) *= -1;
      
      case '{';   idx = [sfind('{') -sfind('}')];  ## way faster than regexp 
      case '[';   idx = [sfind('[') -sfind(']')]; 
      case '(';   idx = [sfind('(') -sfind(')')];
      case '<';   idx = [sfind('<') -sfind('>')];

      case ',';   block  = '';    % don't discard leading ,
                  remain = str(2:end); 
                  return;

      otherwise;  [block,remain] = strtok(str,',');  
                  isempty(remain) || (remain(1) = []);
                  return;
    endswitch
    
    % exclude escaped-brackets (e.g.'\[', '\{', '\"', etc)
    tmp = abs(idx); tmp(1)++;  % don't allow tmp(1) == 1, first bracket can never be escaped
    idx(str(tmp-1) == '\') = [];   

    % virtual closing bracket (handle malformed block)
    idx(end+1) = -(length(str)+1); 

    if idx(2) < 0; 
      close = -idx(2);
      
    else                            % nested block
      [~,o] = sort(abs(idx));       
      idx   = idx(o);
      count = cumsum(sign(idx));        
      count(end) = 0;                   % force close at end of str
      close = -idx(find(count==0, 1));  % find closing of nested block
    end
    
    block  = str( 2 : close-1 );
    remain = strtrim(str( close+1 : end ));

    if !isempty(remain) && remain(1) == ',';  % remove trailing comma (only one)
      remain = remain(2:end);
    endif
  
endfunction

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

function str = _to_string_(str);

  % try converting to inline func from matlab or javascript inline fn string
    try;

      fn  = strtrim(str);
      if fn(1) == '@' || strfind(fn,'=>');
        str = _to_fn_(fn);
      endif
      
    end_try_catch;
    
endfunction

function [fn] = _to_fn_(str);

  if str(1) != '@'; %% assume JS arrow expression
    
    if str(1) != "(";  % must be without parantheses around args
      str = ['@(' _rep_(str,'=>',') ')];
    else  
      str = ['@'  _rep_(str,'=>','  ')];
    endif
      
    % for JS style inline fn, assume element-by-element ops ALWAYS
    strfind(str,'^') && error();
    str = _rep_(str,'**','.^');
    str = regexprep(str,'([*/\\])','.$1');
    str = regexprep(str,'([&|])+','.$1');
  endif
  
  ## NOTE:  if str is octave-style inline fn, do NOT replace ops with .ops
  ##        allow for inline fn to be with matrix ops
  
  fn = str2func(str); 
  
endfunction

function [obj] = _to_struct_ (str);

    obj = struct();

    try

      while str;        
        tok = regexp(str,'[\s,]*"?(.*?)"?\s*:(.*)$','tokens','once');
        key = tok{1};
        [val,str] = _parse_str_(tok{2});
        obj.(key) = val;
      endwhile

    catch err;

        str = regexprep(str,'^\s*,\s*',''); str(25:end) = [];
        str && warning(sprintf('malformed JSON string: "...%s..."\n', str));
        
    end_try_catch

    % look for special case, complex number struct
    keys = fieldnames(obj);

    if ismember(keys,{'re','im'});
       try obj = complex(obj.re,obj.im); end;
    elseif ismember(keys,{'real','imag'});
       try obj = complex(obj.real, obj.imag); end;
    endif
    
endfunction


function [array] = _to_array_(str);

    ok  = !isempty(regexpi(str,'^\s*\[?\s*[-+0-9.ijn]','once')); % 'n' is for "null"

    if ok;    % try numeric conversion    
      array  = regexprep(str,',(?=\s*\[)',';');
      array  = _rep_(array,'null','NaN ');
      [array,ok] = str2num(array);  % this could still fail
    endif

    if ok && isnumeric(array);
      array(abs(array) >= 1e308) *= Inf;   % hack to transmit Inf over JSON
      
    else
      
      array = fromJSON(str);    % NOTE: not garunteed to return cell array

      if iscell(array) && numel(array) && isnumeric(array{1});
        array = _cell2mat_(array);
      endif
      
    endif
    
endfunction  


function [val] = _to_value_ (str);

    [val,ok]     = str2num(str);

    if !ok;
      if strcmpi(str,'null');
          val = [];
      else           % UGGHHH!  giveup
          val = str;  
      endif
    endif
  
endfunction

function a = _cell2mat_(a);
    
    c=a{1};
    
    if cellfun(@(a) isnumeric(a) && size_equal(a,c), a);
      if isscalar(c);
        a = [a{}];
      elseif isvector(c);
        a = cell2mat(a.');
      else
        %TODO: make ND array (is it really necessary?)
      endif
    endif
endfunction


function str1 = _rep_(str,pat,rep,overlap=false);
  str1 = strrep(str,pat,rep,'overlaps',overlap);
endfunction

%!test
%! test=fromJSON();
%! assert(test,[]);

%!test
%! test=fromJSON('');
%! assert(test,[]);

%!test
%! test=fromJSON('4');
%! assert(test,4)

%!test    ## JSON numerical array with Inf and NaN transmission
%! ref=[Inf,-Inf,NaN];
%! test=fromJSON('[1e310,-1e310,null]');
%! assert(test,ref)

%!test  ## JSON-like, with octave numerical notation
%! ref=[Inf,-Inf,NaN,i];
%! test=fromJSON('[Inf,-Inf,NaN,i]');
%! assert(test,ref)

%!test
%! test=fromJSON('{}');
%! assert(test,struct())

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

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

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

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

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

%!test
%! assert(fromJSON("'@sin'"),@sin);

%!test    ## test conversion of JS style arrow function
%! assert(fromJSON("'(x)=>sin(x)'")(3),sin(3));

%!test    ## test element-by-element operation conversion
%!  test = fromJSON('"x=>x**2"');
%!  test = strrep(func2str(test),' ',''); ## to string, strip all whitespace
%!  assert(   isequal(test, '@(x)x.^2' ) );
%!  assert( ! isequal(test, '@(x)x^2'  ) );
  
%!test   ## test element-by-element operation conversion (or lack thereof)
%!  test = fromJSON('"@(x)x^2"');
%!  test = strrep(func2str(test),' ',''); ## to string, strip all whitespace
%!  assert(   isequal(test, '@(x)x^2'  ) );
%!  assert( ! isequal(test, '@(x)x.^2' ) );



%!test
%! test=fromJSON('"test"');
%! assert(test,"test")

%!test   ## escaped quote
%! test=fromJSON('"te\"st\" escaped quote"');
%! assert(test,'te\"st\" escaped quote')

%!test
%! test=fromJSON('["test","list"]');
%! ref={"test","list"};
%! assert(test,ref)

%!test
%! test1=fromJSON('{"a":3}');
%! test2=fromJSON("{        a     :       3            }");
%! ref=struct("a",3);
%! assert(test1,ref)
%! assert(test2,ref)

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

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

%!test
%! test=fromJSON('{a:3,"a":5}');                    % duplicates
%! ref=struct("a",5);
%! assert(test,ref)

%!test
%! test=fromJSON('{a:3,,,    ,     "b"   :5}');      % empty spaces
%! assert(test,struct("a",3,"b",5))

%!test
%! test=fromJSON('[2,{a:3,"b":5}]');
%! assert(test,{2,struct("a",3,"b",5)})

%!test
%! test=fromJSON('[{a:3,"b":5},{a:3}]');
%! ref={struct("a",3,"b",5),struct("a",3)};
%! assert(test,ref)

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

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

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

%!test
%! test=fromJSON('[[[1,2,3,4],[2,3,4,4]],{"fn":1},{"fn"   :3, ybound   :[-Inf,  0]},5,"test"]');
%! ref={[[1 2 3 4];[2 3 4 4]],struct('fn',1),struct('fn',3,'ybound',[-Inf 0]),5,"test"};
%! assert(test,ref)

%!test
%! test=fromJSON(toJSON(ones(2,2,2)));
%! assert(test,{[1 1;1 1],[1 1;1 1]});

%!test
%! o = ones(2,2,2);  o(4) = 4;
%! test=fromJSON(toJSON(o));
%! assert(test,{[1 1;1 4],[1 1;1 1]});



################################## scraps #################################

##              detect 2D array or cell array that looks like 1D numerical array
##    str = strtrim(str);
##    idx = strfind(str,'[');
##    ok  =  isempty(idx) || idx == 1 || (idx(1) == 1 && strfind(str(2:idx(2)),']'));
  



        