classdef Map < handle

properties (getAccess = public, SetAccess = private)
  KeyType   = "char";
  ValueType = "any";
endproperties

properties (Dependent, SetAccess = protected)
  Count
endproperties

properties (Access = private)
  keySet   = cell (1, 0);
  valueSet = cell (1, 0);
endproperties

properties (private)
  numericKeys = false;
endproperties

methods (Access = public)
  function this = Map (varargin)
    if (nargin == 0)
    elseif ((nargin == 2) || ((nargin == 4) && strcmpi(varargin{3}, "UniformValues")))
      key = varargin{1};
      value = varargin{2};
      if (! iscell (key))
        if (isnumeric (key))
          this.numericKeys = true;
          key = num2cell (key);
        else
          key = { key };
        endif
      endif
      if (! iscell (value))
        if (isnumeric (value))
          value = num2cell (value);
        else
          value = { value };
        endif
      endif
      this.keySet = reshape (key, 1, []);
      this.valueSet = reshape (value, 1, []);
      if (numel (this.keySet) != numel (this.valueSet))
        error ("The number of keys and values must be the same.");
      endif

      c = unique (cellfun (@class, this.keySet, "UniformOutput", false));
      if (numel (c) == 1)
        this.KeyType = char (c);
        if (! strcmp (this.KeyType, "char"))
          this.numericKeys = true;
          n = unique (cellfun (@numel, this.keySet));
          if (! (numel (n) == 1 && n == 1))
            error ("Unsupported key specified.");
          endif
        endif
      else
        error ("The specified keys must all be of the same data type.");
      endif
      this = sortKeys (this);
      c = unique( cellfun (@class, this.valueSet, "UniformOutput", false));
      if (numel (c) == 1)
        this.ValueType = char (c);
      else
        this.ValueType = "any";
      endif
      if (nargin == 4)
        if (varargin{4})
          if (strcmp (this.ValueType, "any"))
            error ("When 'UniformValues' is true, all values should belong to the same class.");
          endif
        else
          this.ValueType = "any";
        endif
      endif
    elseif (nargin == 4)
      for i=[1 3]
        switch lower (varargin{i})
          case "keytype"
            this.KeyType = varargin{i+1};
          case "valuetype"
            this.ValueType = varargin{i+1};
          otherwise
            error ("Missing parameter name 'KeyType' or 'ValueType'.");
        endswitch
      endfor
      if (! ismember (this.KeyType, {"char","double", "single", "int32", "uint32", "int64", "uint64"}))
        error ("Unsupported KeyType.");
      endif
      if (! ismember (this.ValueType, {"char","double", "single", "int32", "uint32", "int64", "uint64", "logical", "any"}))
        error ("Unsupported ValueType.");
      endif
    else
      error("Incorrect number of inputs specified.");
    endif
  endfunction
  
  function keySet = keys (this)
    keySet = this.keySet;
  endfunction
  
  function valueSet = values (this, keySet)
    if (nargin == 1)
      valueSet = this.valueSet;
    else
      if (! iscell (keySet))
        if (isnumeric (keySet))
          keySet = num2cell(keySet);
        else
          keySet = { keySet };
        endif
      endif
      valueSet = cell (1, numel (keySet));
      for i=1:numel (valueSet)
        valueSet{i} = subsref (this, substruct ('()', keySet(i)));
      endfor
    endif
  endfunction
  
  function tf = isKey (this, keySet)
    if (! iscell (keySet))
      if (isnumeric (keySet))
        keySet = num2cell(keySet);
      else
        keySet = { keySet };
      endif
    endif
    tf = false(1, numel (keySet));
    for i=1:numel (tf)
      tf(i) = any (findKey (this, keySet{i}));
    endfor
  endfunction
  
  function remove (this, keySet)
    if (! iscell (keySet))
      if (isnumeric (keySet))
        keySet = num2cell(keySet);
      else
        keySet = { keySet };
      endif
    endif
    for i=1:numel (keySet)
      j = findKey (this, keySet{i});
      if (any (j))
        this.keySet(j)   = [];
        this.valueSet(j) = [];
      else
        warning ("The specified key is not present in this container.");
      endif
    endfor
  endfunction
  
  function varargout = size (this, n)
    if (nargin == 1)
      if (nargout <= 1)
        varargout = { [this.Count 1] };
      else
        varargout{1} = this.Count;
        [varargout{2:nargout}] = deal (1);
      endif
    else
      if (n == 1)
        varargout = { this.Count };
      else
        varargout = { 1 };
      endif
    endif
  endfunction
  
  function mapLength = length (this)
    mapLength = this.Count;
  endfunction
  
  function tf = isempty (this, keySet)
    tf = this.Count == 0;
  endfunction
  
  function count = get.Count (this)
    count = uint64 (numel (this.keySet));
  endfunction
  
  function sref = subsref (this, s)
    switch s(1).type
      case "."
        switch s(1).subs
          case "keys"
            sref = keys (this);
          case "values"
            sref = values (this)
          case "size"
            sref = size (this);
          case "length"
            sref = length (this);
          case "isempty"
            sref = isempty (this);
          case "Count"
            sref = this.Count;
          case "KeyType"
            sref = this.KeyType;
          case "ValueType"
            sref = this.ValueType;
          otherwise
            error (["The name '%s' is not an accessible property for " ...
              "an instance of class '%s'."], s(1).subs, class (this));
        endswitch
      case "()"
        i = findKey (this, s(1).subs{1});
        if (any (i))
          sref = this.valueSet{i};
        else
          error ("The specified key is not present in this container.");
        endif
      otherwise
        error ("Only '()' indexing is supported by a containers.Map.");
    endswitch
    if (numel (s) > 1)
      sref = subsref (sref, s(2:end));
    endif
  endfunction
  
  function this = subsasgn (this, s, val)
    if (numel (s) > 1)
      error ("Only one level of indexing is supported by a containers.Map.");
    endif
    switch s(1).type
      case "."
        error ("Properties of an instance of class '%s' are read-only.", class (this));
      case "()"
        key = s(1).subs{1};
        if (! isa (key, this.KeyType))
          error ("Specified key type does not match the type expected for this container.");
        endif
        if (! strcmp (this.ValueType, "any"))
          if (ischar (val) && !strcmp (this.ValueType, "char"))
            error ("Specified value type does not match the type expected for this container.");
          endif
          val = feval (this.ValueType, val);
        endif
        i = findKey (this, key);
        if (any (i))
          this.valueSet{i} = val;
        else
          this = insertKey (this, key, val);
        end
      case '{}'
        error ("Only '()' indexing is supported for assigning values into a containers.Map.");
    endswitch
  endfunction
  
  function newobj = horzcat (varargin)
    error ("Horizontal concatenation is not supported by a containers.Map.");
  endfunction
  
  function newobj = vertcat (varargin)
    % When concatenating maps, the data type of all values must be consistent with the ValueType
    % of the leftmost map.
    keySet = cell (1, 0);
    for i=1:numel (varargin)
      keySet = [keySet varargin{i}.keySet];
    endfor
    valueSet = cell (1, 0);
    for i=1:numel (varargin)
      valueSet = [valueSet varargin{i}.valueSet];
    endfor
    newobj = containers.Map (keySet, valueSet);
  endfunction
  
  function display (this)
    printf ("containers.Map object with properties:\n\n");
    printf (["   Count     : %d\n" ...
             "   KeyType   : %s\n" ...
             "   ValueType : %s\n\n"],
             this.Count, this.KeyType, this.ValueType);
  endfunction
  
endmethods

methods (Access = private)
  function i = findKey (this, key)
    i = cellfun (@(x) isequal (x, key), this.keySet);
  endfunction
  
  function this = insertKey (this, key, val)
    this.keySet{end+1}   = key;
    this.valueSet{end+1} = val;
    this = sortKeys (this);
  endfunction
  function this = sortKeys (this)
    if (this.numericKeys)
      [~, I] = unique(cell2mat(this.keySet));
      this.keySet = this.keySet(I);
      this.valueSet = this.valueSet(I);
    else
      [this.keySet, I] = unique(this.keySet);
      this.valueSet = this.valueSet(I);
    endif
  endfunction
endmethods

endclassdef
