classdef shtable
  
  properties
    vars = struct
  endproperties
  
  methods
    
    function this = shtable (varargin)
      
      if nargin == 0
        return
      endif
      
      % Peel off trailing options
      [opts, args] = peelOffNameValueOptions (varargin, {'VariableNames', 'RowNames'});
      
      % Calling form handling
      nargs = numel (args);
      if nargs == 3 && isequal (args{1}, 'Backdoor')
        % Undocumented form for internal use
        [varNames, varVals] = args{2:3};
      elseif nargs == 4 && isequal (args{1}, 'Size') && isequal (args{3}, 'VariableTypes')
        error('shtable: This constructor form is unimplemented');
      else
        varVals = args;
        if isfield (opts, 'VariableNames')
          varNames = opts.VariableNames;
        else
          varNames = cell (size (args));
          for i = 1:numel (args)
            varNames{i} = inputname (i);
            if isempty (varNames{i})
              varNames{i} = sprintf('Var%d', i);
            end
          end
        end
      end
      
      % Input validation
      if ~iscell (varVals) || (~isvector (varVals) && ~isempty (varVals))
        error('shtable: VariableValues must be a cell vector');
      end
      if ~iscellstr (varNames) || (~isvector (varNames) && ~isempty (varNames))
        error('shtable: VariableNames must be a cellstr vector');
      end
      varNames = varNames(:)';
      varVals = varVals(:)';
      if numel (varNames) ~= numel (varVals)
        error ('shtable: Inconsistent number of VariableNames (%d) and VariableValues (%d)', ...
         numel (varNames), numel (varVals));
      end
      for i = 1:numel (varNames)
        if ~isvarname (varNames{i})
          error ('shtable: Invalid VariableName: ''%s''', varNames{i});
        end
      end
      if ~isempty (varVals)
        nrows = size (varVals{1}, 1);
        for i = 2:numel (varVals)
          if ndims (varVals{i}) > 2
            error (['shtable: Variable values may not have > 2 dimensions; ' ...
              'input %d (%s) has %d'], i, varNames{i}, ndims (varVals{i}));
          endif
          nrows2 = size (varVals{i}, 1);
          if nrows ~= nrows2
            error ('shtable: Inconsistent sizes: var 1 (%s) is %d rows; var %d (%s) is %d rows', ...
              varNames{1}, nrows, i, varNames{i}, nrows2);
          end
        end
      end
      [uqNames, ix] = unique (varNames);
      if numel (uqNames) < numel (varNames)
        ixBad = 1:numel (varNames);
        ixBad(ix) = [];
        error ('shtable: Duplicate VariableNames: %s', strjoin (varNames(ixBad), ', '));
      end

      % Construction
      this.vars = struct;
      for i = 1:numel(varNames)
        s.(varNames{i}) = varVals{i};
      endfor
    end

    function out = subsasgn (this, s, val)
      %SUBSASGN Subscripted assignment

      % Chained subscripts
      chain_s = s(2:end);
      s = s(1);
      if ~isempty (chain_s)
          rhs_in = subsref (this, s);
          rhs = subsasgn (rhs_in, chain_s, val);
      else
          rhs = val;
      end

      out = this;
      switch s(1).type
        case '()'
          error ('shtable.subsasgn: Assignment using ()-indexing is not supported for table');
        case '{}'
          error ('shtable.subsasgn: Assignment using {}-indexing is not supported for table');
        case '.'
          if ~ischar (s.subs)
            error ('shtable.subsasgn: .-index argument must be char; got a %s', ...
              class (s.subs));
          endif
          if isequal (s.subs, 'Properties')
            % Special case for this.Properties access
            error ('shtable.subsasgn: .Properties access is not implemented yet');
          else
            out = setvar (this, s.subs, rhs);            
          endif
      end
    endfunction
    
    function out = subsref (this, s)
      %SUBSREF Subscripted reference
      %
      % tbl.VarName
      % tbl.VarName(ix)
      % tbl.(VarName)
      % tbl(ixRows, ixVars)
      % tbl{ixRow, ixVar}
      % tbl{ixRow, ixVar}(ix)
      %
      % Table supports various forms of indexing that allow you to subset the
      % table or get at the variable values within the table.
      chain_s = s(2:end);
      s = s(1);
      switch s(1).type
        case '()'
          error ('shtable.subsref: ()-indexing not supported');
        case '{}'
          error ('shtable.subsref: {}-indexing not supported');
        case '.'
          name = s.subs;
          if ~ischar (name)
            error ('shtable.subsref: .-reference arguments must be char');
          end
          % Special cases for special properties and other attribute access
          % TODO: should variable names or dimension names take precedence?
          if isequal (name, 'Properties')
            out = getProperties (this);
          else
            out = getvar (this, name);
          endif
      end
      % Chained references
      if ~isempty (chain_s)
        out = subsref (out, chain_s);
      end
    end
  
    function out = setvar (this, varName, value)
      out = this;
      out.vars.(varName) = value;
    endfunction
    
    function out = getvar (this, varName)
      out = this.vars.(varName);
    endfunction
    
  endmethods
  
endclassdef

function [opts, remainingArgs, peeledArgs] = peelOffNameValueOptions (args, knownOpts)
  %PEELOFFNAMEVALUEOPTIONS Peel off name-value options from argins
  %
  % Peels off recognized name-value options from the end of an argument array.
  opts = struct;
  peeledArgs = {};
  while numel (args) >= 2 && ischar (args{end-1}) && ismember (args{end-1}, knownOpts)
    peeledArgs = [peeledArgs args(end-1:end)];
    opts.(args{end-1}) = args{end};
    args(end-1:end) = [];
  end
  remainingArgs = args;
endfunction
