## 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{str} =} toJSON (@var{obj})
## @deftypefnx {} {@var{str} =} toJSON (@var{obj}, @var{prec})
## @deftypefnx {} {@var{str} =} toJSON (@var{obj}, @var{json})
## @deftypefnx {} {@var{str} =} toJSON (@var{obj}, @var{prec}, @var{json})
## @deftypefnx {} {@var{str} =} toJSON (@var{obj}, [], @var{json})
##
##  Converts any @var{obj} into a compact (no spaces) JSON string.
## 
##  An optional numerical @var{prec} specifies the number of significant digits
##  for numerical conversion.  Best used for reducing the size of output string.
##
##  There are no limitation on the numerical class to be converted: double, 
##  float, int, logical, complex, char, etc.  However, except for char and 
##  logical, the info on numerical class is lost in conversion.  
## 
##  Inline function are stored as string, however, reference to external values
##  will be lost.  E.g. consider the following with external reference 'c',
##
##    @(a) a + c
## 
##  The value of 'c' is lost in conversion.
##
##  An optional logical value @var{json} specifies level of JSON compatibility.
##
##  Default for @var{json} is TRUE, for maximum JSON and Octave compatiblity.  
##  JSON spec does not provide for Inf or NaN; nevertheless, Inf and NaN are 
##  converted to JSON string that can convert as such.  Complex numbers are 
##  converted to JSON object string:  '{"re":[[...]], "im":[[..]]}'.
##
##  @var{json} value of FALSE produces a mostly JSON-compatible string.
##  This option is ideal for faster conversion of objects with exclusively real, 
##  finite numbers or strictly Octave-to-Octave transmission (e.g. storage).
##  Inf, NaN, NA and complex numbers are left in Octave-readble format, and 
##  may fail to parse in other programming environments. However, without these 
##  non-real, non-finite numbers, the resulting @var{str} will be JSON compability.
##
## @seealso{fromJSON}
## @end deftypefn

## Author: kmpatel <kmpatel@roc-photonics.com>
## Created: 2020-10-15


function str = toJSON (obj,PREC=[],JSON=true);

  isbool(PREC) && ([PREC,JSON] = {[],PREC}{});
  PREC = abs(PREC);
  
  % go
  str  = _tojson_(obj);
  
  % convert any object into JSON string
  function str = _tojson_(obj);

      if (ND=ndims(obj)) > 2 || iscell(obj);

          if isvector(obj); 
            str = _map_(@(o)_tojson_(o), obj);
            
          else  % make ND array into 1D cell vector of (N-1)D array

            ND == 2 && (D = 1) || (D = ND); % use c-style array

            index.type = "()";
            index.subs = num2cell(repmat(':',1,ND));
            
            for i = 1:size(obj,D);
              index.subs(D) = i;
              str{i} = _tojson_(subsref(obj,index));
            endfor
            
          endif
          
          str = ['[' strjoin(str,',') ']'];

      elseif JSON && iscomplex(obj);
      
          re = real(obj);
          im = imag(obj);
          
          str = _tojson_(struct('re', re, 'im', im));
          
      elseif isnumeric(obj) || isbool(obj); % 1D or 2D matrix
          
          fixit = JSON && !(isbool(obj) || isfinite(obj));
          
          if fixit;
            obj(isnan(obj)) = NaN;    % make uniform  all NaN, nan and NA
          endif
          
          str = mat2str(obj,PREC);
          
          if iscolumn(obj);
             str = _rep_(str,';',',');
             
          else
             str = _rep_(str,' ',',');
                    
             if !isvector(obj);
               str = ['[' _rep_(str,';','],[') ']'];
             endif
          endif
          
          if fixit;
              % GRRR! JSON is not IEEE754 complaint
              %  these are hacks to transmit Inf and NaN
              str = _rep_(str,'Inf','1e999');
              str = _rep_(str,'NaN','null');
          endif

      elseif isstruct(obj);

          if( numel(obj) > 1 );   ## TODO: this could be more efficient for 1D struct array
            str = _tojson_(num2cell(obj));

          else
            keys = fieldnames(obj);
            vals = struct2cell(obj);
            
            str = _map_(@(k,v)['"',k,'":',_tojson_(v)], keys, vals); 
            str = ["{" strjoin(str,",") "}"];
            
          endif
          
      elseif ischar(obj);
 
          if !length(obj);
              str = '""';
              
          elseif (o=obj([1,end])) == '"' || o == '[]' || o == '{}';    % already quoted/object/struct string
              str = obj;
              
          elseif rows(obj) > 1;    % 2D array of chars => column vector of strings
              str = _tojson_(mat2cell(obj,ones(1,rows(obj)),columns(obj)));
              
          else                      % otherwise, quote the str, \esc all double-quotes
              str = ['"' _rep_(obj,'"','\"') '"']; 
          end

      elseif is_function_handle(obj);
      
          str = func2str(obj);
         
          if str(1) != "@";
            str = ['"@' str '"'];
          end

      else
      
          str =  ['"[' class(obj) ']"'];
         
      endif

  endfunction

endfunction

function str1 = _rep_(str,pat,rep);
  str1 = strrep(str,pat,rep,'overlaps',false);
endfunction
 
function obj = _map_(fn,varargin);
  obj = cellfun(fn,varargin{},'UniformOutput',false);
endfunction 

%!test
%!  test = toJSON(4);
%!  assert(test,"4")

%!test
%!  test = toJSON("4");
%!  assert(test,'"4"')

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

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

%!test
%!  test = toJSON([1,2,3;13,14,15]);
%!  assert(test,'[[1,2,3],[13,14,15]]')

%!test  ## test complex number 
%!  test = toJSON(i);
%!  assert(test,'{"re":0,"im":1}');

%!test    ## test complex number 1D array 
%!  test = toJSON([i,1]);
%!  assert(test,'{"re":[0,1],"im":[1,0]}');
##%!  assert(test,'[{"re":0,"im":1},{"re":1,"im":0}]');

%!test    ## test complex number cell array 
%!  test = toJSON({i,1});
%!  assert(test,'[{"re":0,"im":1},1]');

%!test
%!  test = toJSON([i 1;2 i*3]); ## test complex number 2D array
%!  assert(test,'{"re":[[0,1],[2,0]],"im":[[1,0],[0,3]]}');

%!test
%!  b = false(2,2);  b(2) = true;
%!  test = toJSON(b);
%!  assert(test,'[[false,false],[true,false]]')

%!test 
%!  test = toJSON([Inf,-inf,nan,NaN,NA]);
%!  assert(test,'[1e999,-1e999,null,null,null]')

%!test  ## test JSON=true (max JSON compatibilty)
%!  test = toJSON({[Inf,-inf,nan,NaN,NA],1+2i},true);
%!  assert(test,'[[1e999,-1e999,null,null,null],{"re":1,"im":2}]')

%!test  ## test JSON=false (JSON-like, limited compatibilty)
%!  test = toJSON({[Inf,-inf,nan,NaN,NA],1+2i},false);
%!  assert(test,'[[Inf,-Inf,NaN,NaN,NA],1+2i]')

%!test  ## test precsion
%!  assert(toJSON(pi*(1:4),3),"[3.14,6.28,9.42,12.6]")

%!test ## test char arrayfun
%! test = toJSON(["a";"bc";"defg"]);
%! assert(test, '["a   ","bc  ","defg"]');

%!test ## test char arrayfun
%! test = toJSON(["a";"bc";"defg"]');
%! assert(test, '["abd"," ce","  f","  g"]');

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

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

%!test
%!  test = toJSON({1,2,3,"a"});
%!  assert(test,'[1,2,3,"a"]')

%!test
%!  test = toJSON({[1,2,3];[3,4,5]});
%!  assert(test,'[[1,2,3],[3,4,5]]')

%!test
%!  ndmat = ones(2,2,3);
%!  ndmat([4,9]) = [4,9];
%!  test = toJSON(ndmat);
%!  assert(test,'[[[1,1],[1,4]],[[1,1],[1,1]],[[9,1],[1,1]]]')
%!  ndmat = ones(2,2,2,3);
%!  ndmat([4,9,21]) = [4,9,21];
%!  test = toJSON(ndmat);
%!  assert(test,'[[[[1,1],[1,4]],[[1,1],[1,1]]],[[[9,1],[1,1]],[[1,1],[1,1]]],[[[1,1],[1,1]],[[21,1],[1,1]]]]')

%!test
%!  s = struct("a",3);
%!  test = toJSON(s);
%!  assert(test,'{"a":3}')

%!test
%!  test = toJSON([1,2,3]);
%!  assert(test,'[1,2,3]')
%!  s = struct("a",3);
%!  test = toJSON([s,s]);
%!  assert(test,'[{"a":3},{"a":3}]')

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

%!test
%!  test = toJSON(@sin);
%!  assert(test,'"@sin"')
    
    
################################## scraps ##############################33

##          if isvector(obj);       % choose to convert only 1D array to struct array
##            re = num2cell(re(:)); % TODO: reconsider this choice
##            im = num2cell(im(:));
##          endif
              