########################################################################
##
## Copyright (C) 2022 The Octave Project Developers
##
## See the file COPYRIGHT.md in the top-level directory of this
## distribution or <https://octave.org/copyright/>.
##
## This file is part of Octave.
##
## Octave 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.
##
## Octave 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 Octave; see the file COPYING.  If not, see
## <https://www.gnu.org/licenses/>.
##
########################################################################

classdef dictionary

  ## -*- texinfo -*-
  ## @deftypefn  {} {@var{d} =} dictionary ()
  ## @deftypefnx {} {@var{d} =} dictionary (@var{keys}, @var{vals})
  ## Construct a new dictionary object.
  ##
  ## @seealso{struct, containers.Map}
  ## @end deftypefn

  properties (private)
    map = struct ();

    all_keys = {};

    key_type = "";
    value_type = "";
  endproperties

  methods (Access = public)

    function this = dictionary (varargin)

      if (nargin == 0)
        ## Unconfigured dictionary with no keys or values
      elseif (numel (varargin) == 1 && isa (varargin{1}, "dictionary"))
        ## Copy constructor
        this = varargin{1};
      elseif (mod (numel (varargin), 2) == 0)
        for p = 1:2:numel (varargin)
          k = varargin{p};
          v = varargin{p+1};
          if (numel (k) != numel (v) && numel (v) != 1)
            error ("dictionary: number of keys and values are not compatible");
          endif
          ## Configure data type of keys and values
          if (isempty (this.key_type))
            this.key_type = class (k);
            this.value_type = class (v);
          endif
          ## Insert key/value pairs after conversion
          for i = 1:numel (k)
            key = k(i);
            if (!isa (key, this.key_type))
              key = encode_data (this.key_type, k(i));
            endif
            h = num2hex (keyHash (key));
            if (! isfield (this.map, h))
              this.all_keys{end+1} = key;
            endif
            if (numel (v) == 1)
              if (isa (v, this.value_type))
                this.map.(h) = v;
              else
                this.map.(h) = encode_data (this.value_type, v);
              endif
            else
              if (isa (v, this.value_type))
                this.map.(h) = v(i);
              else
                this.map.(h) = encode_data (this.value_type, v(i));
              endif
            endif
          endfor
        endfor
      else
        error ("dictionary: a value is expected after a key");
      endif

    endfunction

    function e = entries (this, fmt)

      ## -*- texinfo -*-
      ## @deftypefn {} {} dictionary.entries (@var{fmt})
      ## @end deftypefn

      if (nargin < 2)
        fmt = "struct";  # default should be "table"
      else
        if (! ischar (fmt) && ! isstring (fmt))
          error ("dictionary: 'format' option has to be a string");
        endif
      endif
      k = keys (this, "cell");
      v = values (this, "cell");
      if (startsWith ("cell", fmt, "IgnoreCase", true))
        e = [k, v];
      elseif (startsWith ("struct", fmt, "IgnoreCase", true))
        e = struct ("Key", k, "Value", v);
      elseif (startsWith ("table", fmt, "IgnoreCase", true))
        error ("dictionary: 'table' format option is not implemented");
      else
        error ("dictionary: 'format' option can only be 'struct', 'cell' or 'table'");
      endif

    endfunction

    function k = keys (this, fmt)

      ## -*- texinfo -*-
      ## @deftypefn {} {} dictionary.keys (@var{fmt})
      ## @end deftypefn

      if (! isConfigured (this))
        error ("dictionary: dictionary is not configured");
      endif

      if (nargin < 2)
        fmt = "uniform";
      else
        if (! ischar (fmt) && ! isstring (fmt))
          error ("dictionary: 'format' option has to be a string");
        endif
      endif
      if (startsWith ("cell", fmt, "IgnoreCase", true))
        k = this.all_keys(:);
      elseif (startsWith ("uniform", fmt, "IgnoreCase", true))
        try
          k = [this.all_keys{:}].';
        catch
          error ("dictionary: cannot concatenate keys. Use 'cell' option.");
        end_try_catch
      else
        error ("dictionary: 'format' option can only be 'cell'");
      endif

    endfunction

    function v = values (this, fmt)

      ## -*- texinfo -*-
      ## @deftypefn {} {} dictionary.values (@var{fmt})
      ## @end deftypefn

      if (! isConfigured (this))
        error ("dictionary: dictionary is not configured");
      endif

      if (nargin < 2)
        fmt = "uniform";
      else
        if (! ischar (fmt) && ! isstring (fmt))
          error ("dictionary: 'format' option has to be a string");
        endif
      endif
      if (startsWith ("cell", fmt, "IgnoreCase", true))
        v = struct2cell (this.map);
      elseif (startsWith ("uniform", fmt, "IgnoreCase", true))
        v = struct2cell (this.map);
        try
          v = [v{:}].';
        catch
          error ("dictionary: cannot concatenate values. Use 'cell' option.");
        end_try_catch
      else
        error ("dictionary: 'format' option can only be 'cell'");
      endif

    endfunction

    function tf = isKey (this, key)

      ## -*- texinfo -*-
      ## @deftypefn {} {} dictionary.isKey (@var{key})
      ## @end deftypefn

      if (! isConfigured (this))
        error ("dictionary: dictionary is not configured");
      endif
      tf = false (size (key));
      for k = 1:numel (key)
        cur_key = key(k);
        if (!isa (cur_key, this.key_type))
          cur_key = encode_data (this.key_type, cur_key);
        endif
        h = num2hex (keyHash (cur_key));
        tf(k) = isfield (this.map, h);
##        try
##          this.map.(h);
##          tf = true;
##        catch
##          tf = false;
##        end_try_catch
      endfor
    endfunction

    function tf = isConfigured (this)

      ## -*- texinfo -*-
      ## @deftypefn {} {} dictionary.isConfigured ()
      ## @end deftypefn

      tf = ! isempty (this.key_type);
    endfunction

    function [keyType, valueType] = types (this)

      ## -*- texinfo -*-
      ## @deftypefn {} {} dictionary.types ()
      ## @end deftypefn

      if (! isConfigured (this))
        keyType = valueType = "missing";  # replace with <missing> string scalar
      else
        keyType = this.key_type;
        valueType = this.value_type;
      endif

    endfunction

    function count = numEntries (this)

      ## -*- texinfo -*-
      ## @deftypefn {} {} dictionary.numEntries ()
      ## @end deftypefn

      count =  numel (this.all_keys);
    endfunction

    function sref = subsref (this, s)

      switch (s(1).type)
        case "."
          extra = {};
          if (numel (s) > 1 && strcmp (s(2).type, "()"))
            if (! isempty (s(2).subs))
              extra = s(2).subs;
            endif
            s(2) = [];
          endif
          switch (s(1).subs)
            case "keys"
              sref = keys (this, extra{:});
            case "values"
              sref = values (this, extra{:});
            case "types"
              sref = types (this, extra{:});
            case "numEntries"
              sref = numEntries (this, extra{:});
            case "isConfigured"
              sref = isConfigured (this, extra{:});
            case "isKey"
              sref = isKey (this, extra{:});
            otherwise
              error ("dictionary: unknown property '%s'", s(1).subs);
          endswitch
        case "()"
          if (isempty (s(1).subs))
            error ("dictionary: no key specified");
          endif
          if (! isConfigured (this))
            error ("dictionary: dictionary not configured");
          endif
          key = s(1).subs{1};
          sref = cell (size (key));
          for k = 1:numel (key)
            cur_key = key(k);
            if (!isa (cur_key, this.key_type))
              cur_key = encode_data (this.key_type, cur_key);
            endif
            h = num2hex (keyHash (cur_key));
            if (isfield (this.map, h))
              sref{k} = this.map.(h);
            else
              error ("dictionary: unknown key");
            endif
##            try
##              sref = this.map.(h);
##            catch
##              error ("dictionary: unknown key");
##            end_try_catch
          endfor
          try
            sref = [sref{:}];
          catch
            error ("dictionary: cannot concatenate values");
          end_try_catch
          sref = reshape (sref, size (key));
        otherwise
          error ("dictionary: '{}' indexing is not supported");
      endswitch
      if (numel (s) > 1)
        sref = subsref (sref, s(2:end));
      endif

    endfunction

    function this = subsasgn (this, s, val)

      if (numel (s) > 1)
        error ("dictionary: only one level of indexing is supported");
      endif
      switch (s(1).type)
        case "()"
          key = s(1).subs{1};
          if (! isConfigured (this))
            this.key_type = class (key);
            this.value_type = class (val);
          endif
          ## TODO ## key and val can be an arrays
          if (numel (key) != numel (val) && numel (val) != 1 && numel (val) != 0)
            error ("dictionary: number of keys and values are not compatible");
          endif
          for k = 1:numel (key)
            cur_key = key(k);
            if (!isa (cur_key, this.key_type))
              cur_key = encode_data (this.key_type, cur_key);
            endif
            h = num2hex (keyHash (cur_key));
            if (isa (val, "double") && isequal (size (val), [0 0]))
              if (isfield (this.map, h))
                this.map = rmfield (this.map, h);
                ## finding index of key to delete is O(N): another hashmap for indices?
                this.all_keys(cellfun (@(x) isequal (x, cur_key), this.all_keys)) = [];
              else
                ## non existing key - ignore
              endif
              continue;
            endif
            if (! isfield (this.map, h))
              this.all_keys{end+1} = cur_key;
            endif
##            try
##              this.map.(h);
##            catch
##              this.all_keys{end+1} = cur_key;
##            end_try_catch
            if (numel (val) == 1)
              if (isa (val, this.value_type))
                this.map.(h) = val;
              else
                this.map.(h) = encode_data (this.value_type, val);
              endif
            else
              if (isa (val, this.value_type))
                this.map.(h) = val(k);
              else
                this.map.(h) = encode_data (this.value_type, val(k));
              endif
            endif
          endfor
        otherwise
          error ("dictionary: only '()' indexing is supported for assigning values");
      endswitch

    endfunction

    function newobj = horzcat (varargin)
      error ("dictionary: horizontal concatenation is not allowed");
    endfunction

    function newobj = vertcat (varargin)
      error ("dictionary: vertical concatenation is not allowed");
    endfunction

    function disp (this)
      if (! isConfigured (this))
        printf ("dictionary object not configured.\n");
      elseif (numEntries (this) == 0)
        printf ("dictionary object (%s --> %s) with no entries.\n",
          this.key_type, this.value_type);
      else
        entry_str = ifelse (numEntries (this) == 1, "entry", "entries");
        printf ("dictionary object (%s --> %s) with %d %s:\n\n",
          this.key_type, this.value_type, numEntries (this), entry_str);
          for i = 1:numEntries (this)
            key = this.all_keys{i};
            h = num2hex (keyHash (key));
            if (! ischar (key)), key = sprintf ("<%s>", class (key)); end
            val = this.map.(h);
            if (! ischar (val)), val = sprintf ("<%s>", class (val)); end
            printf ("  %s --> %s\n", key, val);
          endfor
      endif
    endfunction

  endmethods

endclassdef

function ret = encode_data (t, d)

  if (isa (d, t))
    return;
  endif
  switch (t)
    case {"cell", "struct"}
      error ("dictionary: invalid conversion from '%s' to '%s'", ...
        class (d), t);
    case {"char"}
      if (isnumeric (d))
        ret = num2str (d);
      else
        ret = feval (t, d);
      endif
    otherwise
      ret = feval (t, d);
  endswitch

endfunction

## Test unconfigured dictionary
%!test
%! d = dictionary ();
%! assert (isConfigured (d), false);
%! assert (d.isConfigured, false);
%! assert (length (d), 1);
%! assert (numel (d), 1);
%! assert (ndims (d), 2);
%! assert (size (d), [1 1]);
%! assert (numEntries (d), 0);
%! assert (d.numEntries, 0);
%! assert (types (d), "missing");
%! assert (d.types, "missing");
%! [key_type, val_type] = types (d);
%! assert (key_type, "missing");
%! assert (val_type, "missing");

## Test constructor
%!test
%! d = dictionary (dictionary);
%! assert (class (d), "dictionary");
%!test
%! d = dictionary ("a", 1);
%! assert (isConfigured (d), true);
%! assert (numEntries (d), 1);
%! assert (types (d), "char");
%! [key_type, val_type] = types (d);
%! assert (key_type, "char");
%! assert (val_type, "double");
%!test
%! d = dictionary ("a", 1, "b", 2, "c", 3);
%! assert (isConfigured (d), true);
%! assert (numEntries (d), 3);
%! assert (types (d), "char");
%! [key_type, val_type] = types (d);
%! assert (key_type, "char");
%! assert (val_type, "double");
%! assert (keys (d), strvcat ("a", "b", "c"));
%! assert (keys (d, "cell"), {"a"; "b"; "c"});
%! assert (values (d), [1 2 3]');
%! assert (values (d, "cell"), {1; 2; 3});
%! assert (entries (d), struct ("Key", {"a"; "b"; "c"}, "Value", {1; 2; 3}));
%! assert (entries (d, "struct"), struct ("Key", {"a"; "b"; "c"}, "Value", {1; 2; 3}));
%! assert (entries (d, "cell"), {"a", 1; "b", 2; "c", 3});
%! assert (isKey (d, "a"), true);
%! assert (isKey (d, "d"), false);

## Test configuration
%!test
%! d = dictionary;
%! assert (isConfigured (d), false);
%! d(1) = "a";
%! assert (isConfigured (d), true);
%! [key_type, val_type] = types (d);
%! assert (key_type, "double");
%! assert (val_type, "char");

%!test
%! d = dictionary ([], '');
%! assert (isConfigured (d), true);
%! [key_type, val_type] = types (d);
%! assert (key_type, "double");
%! assert (val_type, "char");

## Test isKey/numEntries/keys/values methods
%!test
%! d = dictionary ("a", 1, "b", 2);
%! assert (numEntries (d), 2);
%! assert (d("a"), 1);
%! assert (isKey (d, "c"), false);
%! d("c") = 3;
%! assert (numEntries (d), 3);
%! assert (d("c"), 3);
%! assert (isKey (d, "c"), true);
%! d("a") = pi;
%! assert (numEntries (d), 3);
%! assert (d("a"), pi);
%! assert (values (d), [pi 2 3]');
%! assert (keys (d, "cell"), {"a"; "b"; "c"});
%! d("b") = [];
%! assert (numEntries (d), 2);
%! assert (keys (d, "cell"), {"a"; "c"});
%! assert (values (d), [pi; 3]);

## Test cell keys
%!test
%! d = dictionary ({"a"}, 1, {"b"}, 2);
%! d({"c"}) = 3;
%! [key_type, val_type] = types (d);
%! assert (key_type, "cell");
%! assert (val_type, "double");
%! assert (d({"c"}), 3);

## Test struct keys
%! d = dictionary (struct ("a", 1), {1}, struct ("a", 2), {2});
%! d(struct ("b", 1)) = {3};
%! [key_type, val_type] = types (d);
%! assert (key_type, "struct");
%! assert (val_type, "cell");
%! assert (d(struct ("a", 2)), {2});

## Test vectorized susbref
%! d = dictionary (1, 1, 2, 2, 3, 3, 4, 4);
%! assert (d([1 2;3 4]), [1 2;3 4]);

## Test input validation
%!error dictionary (1);
%!error dictionary ("a");
%!error dictionary (1, "a", 2);
%!error keys (dictionary);
%!error values (dictionary);
%!error keys (dictionary ("a", 1), {"cell"});
%!error keys (dictionary ("a", 1), "other");
%!error values (dictionary ("a", 1), {"cell"});
%!error values (dictionary ("a", 1), "other");
%!error entries (dictionary ("a", 1), struct);
%!error entries (dictionary ("a", 1), "other");
%!error isKey (dictionary, "a");
%!error
%! d = dictionary ("a", 1);
%! d("a") = '';
%!error
%! d = dictionary ({"a"}, 1);
%! d(2) = 3;
%!error
%! d = dictionary ({"a"}, {1});
%! d({2}) = 3;
%!error
%! d = dictionary (struct ("a", 1,'b',2), {1}, struct ("a", 2), {2});
%! keys(d)

## Test concatenation
%!error
%! d = dictionary;
%! [d, d];
%!error
%! d = dictionary;
%! [d; d];

## Test vectorized subsref
%!error
%! d = dictionary (1, struct ("a", 1), 2, struct ("a", 1, "b", 2));
%! d([1 2]);
