package MSDP;

use strict;
use warnings;

use Carp;


use constant {
  MSDP_VAR => chr(1),
  MSDP_VAL => chr(2),
  MSDP_TABLE_OPEN => chr(3),
  MSDP_TABLE_CLOSE => chr(4),
  MSDP_ARRAY_OPEN => chr(5),
  MSDP_ARRAY_CLOSE => chr(6),
  EOF => chr(0),
};


#######################################################################
# Public functions                                                    #
#######################################################################
sub new {
  my $self = shift;
  my $class = ref($self) || $self;

  my $data = shift;

  return bless { }, $class;
}


sub decode {
  my $self = shift;
  my ($data) = (@_);

  $self->{'data'} = $data;
  $self->{'current_pos'} = 0;

  my %variables;

  my $next_tok = $self->_get_next_token_or_eof();
  while ($next_tok ne EOF) {
    $self->_put_back($next_tok);
    my ($name, $value) = $self->_get_var();
    $variables{$name} = $value;
    $next_tok = $self->_get_next_token_or_eof();
  }

  return \%variables;
}


sub encode {
  my $self = shift;
  my ($data) = (@_);

  if (ref($data) ne 'HASH') {
    Carp::croak("Invalid data type, expecting hash reference");
  }

  my $encoded = "";
  while (my ($key, $value) = each(%$data)) {
    $encoded .= _encode_var($key, $value);
  }
  return $encoded;
}


sub encode_command {
  my $self = shift;

  if (scalar @_ < 1) {
    Carp::croak("Command not specified");
  }
  my $command = shift;

  my $encoded = MSDP_VAR . $command;
  foreach my $arg (@_) {
    $encoded .= MSDP_VAL . _encode_value($arg);
  }

  return $encoded;
}



#######################################################################
# Private functions                                                   #
#######################################################################

#######################################################################
# Decoding                                                            #
#######################################################################
sub _get_var {
  my $self = shift;

  $self->_expect(MSDP_VAR);
  my $name = $self->_get_textual_token();
  $self->_expect(MSDP_VAL);
  my $value = $self->_get_value();

  # Handle the case of a multi-valued variable (which becomes an array)
  # eg: MSDP_VAR variable MSDP_VAL value1 MSDP_VAL value2
  my $token = $self->_get_next_token_or_eof();
  while ($token eq MSDP_VAL) {
    unless (ref($value)) {
      $value = [ $value ];
    }

    my $next_value = $self->_get_value();
    push(@$value, $next_value);
    $token = $self->_get_next_token_or_eof();
  }

  $self->_put_back($token);

  return ($name, $value);
}


sub _get_value {
  my $self = shift;

  my $token = $self->_get_next_token();
  if ($token eq MSDP_ARRAY_OPEN) {
    return $self->_get_array()
  } elsif ($token eq MSDP_TABLE_OPEN) {
    return $self->_get_table();
  }
  return $token;
}


sub _get_array {
  my $self = shift;

  my @values;
  while (1) {
    my $token = $self->_get_next_token();
    if ($token eq MSDP_ARRAY_CLOSE) {
      return \@values;
    } elsif ($token eq MSDP_VAL) {
      push (@values, $self->_get_value());
    } else {
      # FIXME: Better reporting
      Carp::croak("Got unexpected token, expecting MSDP_VAL or MSDP_ARRAY_CLOSE");
    }
  }
}


sub _get_table {
  my $self = shift;

  my %values;
  while (1) {
    my $token = $self->_get_next_token();
    if ($token eq MSDP_TABLE_CLOSE) {
      return \%values;
    } elsif ($token eq MSDP_VAR) {
      $self->_put_back($token);
      my ($name, $value) = $self->_get_var();
      $values{$name} = $value;
    } else {
      # FIXME: Better reporting
      Carp::croak("Got unexpected token, expecting MSDP_VAR or MSDP_TABLE_CLOSE");
    }
  }
}


sub _expect {
  my $self = shift;
  my ($expected_token) = @_;

  if ($self->_get_next_token_or_eof() ne $expected_token) {
    # FIXME: Describe better the problem: what we got, and what should
    # have been found
    Carp::croak("Got unexpected token");
  }
}


sub _get_textual_token {
  my $self = shift;

  my $token = $self->_get_next_token();
  if (_is_special_char($token)) {
    # FIXME: Better reporting
    Carp::croak("Expecting a string, got a control token");
  }
  return $token;
}


sub _get_next_token {
  my $self = shift;

  my $token = $self->_get_next_token_or_eof();
  if ($token eq EOF) {
    Carp::croak("Unexpected end of string");
  }
  return $token;
}


sub _put_back {
  my $self = shift;
  my ($token) = @_;

  $self->{'next_token'} = $token;
}


sub _get_next_token_or_eof {
  my $self = shift;

  if (defined $self->{'next_token'}) {
    my $token = $self->{'next_token'};
    $self->{'next_token'} = undef;
    return $token;
  }

  my $char = $self->_get_next_char();
  if ($char eq EOF) {
    return EOF;
  }

  if (_is_special_char($char)) {
    return $char;
  }

  my $name = $char;
  while (1) {
    $char = $self->_get_next_char();

    if ($char eq EOF) {
      return $name;
    }
    if (_is_special_char($char)) {
      --$self->{'current_pos'};
      return $name;
    }

    $name .= $char;
  }
}


sub _get_next_char {
  my $self = shift;

  if ($self->{'current_pos'} == length($self->{'data'})) {
    return EOF;
  }

  my $char = substr($self->{'data'}, $self->{'current_pos'}++, 1);
  return $char;
}


sub _is_special_char {
  my ($char) = @_;

  return (($char ge MSDP_VAR && $char le MSDP_ARRAY_CLOSE)
          || $char eq EOF);
}


#######################################################################
# Encoding                                                            #
#######################################################################
sub _encode_var {
  my ($key, $value) = @_;

  return MSDP_VAR . $key . MSDP_VAL . _encode_value($value);
}


sub _encode_value {
  my ($value) = @_;

  my $type = ref($value);
  if ($type eq '') {
    return $value;
  } elsif ($type eq 'SCALAR') {
    return $$value;
  } elsif ($type eq 'ARRAY') {
    return _encode_array($value);
  } elsif ($type eq 'HASH') {
    return _encode_table($value);
  } else {
    Carp::croak("Unsupported type $type");
  }
}


sub _encode_array {
  my ($array) = @_;

  my $encoded = MSDP_ARRAY_OPEN;
  foreach my $element (@$array) {
    $encoded .= MSDP_VAL . _encode_value($element);
  }
  $encoded .= MSDP_ARRAY_CLOSE;

  return $encoded;
}


sub _encode_table {
  my ($hash) = @_;

  my $encoded = MSDP_TABLE_OPEN;
  while (my ($key, $value) = each(%$hash)) {
    $encoded .= MSDP_VAR . $key . MSDP_VAL . _encode_value($value);
  }
  $encoded .= MSDP_TABLE_CLOSE;

  return $encoded;
}


1;
