package Asterisk::Manager;

$VERSION = '0.02';
#v0.02 
#used select and nonbuffered read/write for asynchronous eventpending implementation,
#some debugging messages, export version

require 5.004;

use Asterisk;
use IO::Socket;
use IO::Select;
use Digest::MD5;

use strict;
use warnings;
use vars qw/$VERSION/;

=head1 NAME

Asterisk::Manager - Asterisk Manager Interface

=head1 SYNOPSIS

use Asterisk::Manager;

my $astman = new Asterisk::Manager;

$astman->user('username');

$astman->secret('test');

$astman->host('localhost');

$astman->connect || die "Could not connect to " . $astman->host . "!\n";

$astman->disconnect;

=head1 DESCRIPTION

This module provides a simple interface to the asterisk manager interface.

=cut

my $EOL = "\r\n";
my $BLANK = $EOL x 2;

sub version { $VERSION; }

sub new {
	my ($class, %args) = @_;

	my $self = {};
	$self->{_CONNFD} = undef;
	$self->{_PROTOVERS} = undef;
	$self->{_ERRORSTR} = undef;
	$self->{_HOST} = 'localhost';
	$self->{_PORT} = 5038;
	$self->{_USER} = undef;
	$self->{_SECRET} = undef;
	$self->{_EVENTCB} = {};
	$self->{_DEBUG} = 0;
	$self->{_CONNECTED} = 0;
	bless $self, ref $class || $class;
	return $self;
}

sub DESTROY { }

sub user {
	my ($self, $user) = @_;

	if ($user) {
		$self->{_USER} = $user;
	}

	return $self->{_USER};
}

sub secret {
	my ($self, $secret) = @_;

	if ($secret) {
		$self->{_SECRET} = $secret;
	}

	return $self->{_SECRET};
}

sub host {
	my ($self, $host) = @_;

	if ($host) {
		$self->{_HOST} = $host;
	}

	return $self->{_HOST};
}

sub port {
	my ($self, $port) = @_;

	if ($port) {
		$self->{_PORT} = $port;
	}

	return $self->{_PORT};
}

sub connected {
	my ($self, $connected) = @_;

	if (defined($connected)) {
		$self->{_CONNECTED} = $connected;
	}

	return $self->{_CONNECTED};
}

sub error {
	my ($self, $error) = @_;

	if ($error) {
		$self->{_ERRORSTR} = $error;
	}

	return $self->{_ERRORSTR};
}

sub debug {
	my ($self, $debug) = @_;

	if ($debug) {
		$self->{_DEBUG} = $debug;
	}

	return $self->{_DEBUG};
}

sub connfd {
	my ($self, $connfd) = @_;

	if ($connfd) {
		$self->{_CONNFD} = $connfd;
		$self->{_SELECT} = IO::Select->new($connfd);
	}

	return $self->{_CONNFD};
}

sub read_from_socket {
	my ($self, $connfd, $term) = @_;

  my $buf;
  while (not $self->{_READBUFFER} =~ /$term/) {
    if (not sysread($connfd, $buf, 1000)) {
      $self->error("Read error\n");
      return undef;
    }
    $self->{_READBUFFER}.=$buf;
  }
  my $out;
  ($out, $self->{_READBUFFER}) = split(/$term/, $self->{_READBUFFER}, 2);
  return $out;
}

sub read_response {
	my ($self, $connfd) = @_;

	my @response;

	if (!$connfd) {
		$connfd = $self->connfd;
	}

  my $input = $self->read_from_socket($connfd, $BLANK);
  if (not $input) { return undef }
  foreach my $line (split(/$EOL/, $input)) {
		if (wantarray) {
			$line =~ s/$EOL//g;
			push(@response, $line) if $line;
		} else {
			$response[0] .= $line;
		}

	}

  if ($self->{_DEBUG}) {
    print("RESPONSE: ");
    foreach (@response) { print("$_|"); }
    print("\n");
  }
	return wantarray ? @response : $response[0];
}

sub connect {
	my ($self) = @_;

	my $host = $self->host;
	my $port = $self->port;
	my $user = $self->user;
	my $secret = $self->secret;
	my %resp;

	my $conn = new IO::Socket::INET( Proto => 'tcp',
					 PeerAddr => $host,
					 PeerPort => $port
					);
	if (!$conn) {
		$self->error("Connection refused ($host:$port)\n");
		return undef;
	}
	$conn->autoflush(1);

	$self->{_READBUFFER} = "";
  my $input = $self->read_from_socket($conn, $EOL);
  if (not $input) { return undef }
	my ($manager, $version) = split('/', $input);

	if ($manager !~ /Asterisk Call Manager/) {
    $self->error("Unknown Protocol\n");
		return undef;
	}

	$self->{_PROTOVERS} = $version;
	$self->connfd($conn);

	# check if the remote host supports MD5 Challenge authentication
	my %authresp = $self->sendcommand( Action => 'Challenge',
					   AuthType => 'MD5'
					 );

	if (($authresp{Response} eq 'Success')) {
		# do md5 login
		my $md5 = new Digest::MD5;
		$md5->add($authresp{Challenge});
		$md5->add($secret);
		my $digest = $md5->hexdigest;
		%resp = $self->sendcommand(  Action => 'Login',
					     AuthType => 'MD5',
					     Username => $user,
					     Key => $digest
					  );
	} else {
		# do plain text login
		%resp = $self->sendcommand(  Action => 'Login',
					     Username => $user,
					     Secret => $secret
					  );

	}

	if ( ($resp{Response} ne 'Success') && ($resp{Message} ne 'Authentication accepted') ) {
		$self->error("Authentication failed for user $user\n");
		return undef;
	}

	$self->connected(1);

	return $conn;
}

sub astman_h2s {
	my ($self, %thash) = @_;

	my $tstring = '';

	foreach my $key (keys %thash) {
		$tstring .= $key . ': ' . $thash{$key} . ${EOL};
	}

	return $tstring;
}

sub astman_s2h {
	my ($self, $tstring) = @_;

	my %thash;

	foreach my $line (split(/$EOL/, $tstring)) {
		if ($line =~ /(\w*):\s*(\w*)/) {
			$thash{$1} = $2;
		}
	}

	return %thash;
}

#$want is how you want the data returned
#$want = 0 (default) returns the results in a hash
#$want = 1 returns the results in a large string
#$want = 2 returns the results in an array
sub sendcommand {
	my ($self, %command, $want) = @_;

	if (!defined($want)) {
		$want = 0;
	}
	
	my $conn = $self->connfd || return;
	my $cstring = $self->astman_h2s(%command);

  if ($self->{_DEBUG}) {
    print("COMMAND: $cstring");
  }
  syswrite($conn, "$cstring$EOL");

	if ($want == 1) {
		my $response = $self->read_response($conn);
		return $response;
	}

	my @resp = $self->read_response($conn);
  return undef unless @resp;

	if ($want == 2) {
		return @resp;
	} else {
		return map { splitresult($_) } @resp;
	}
}

sub setcallback {
	my ($self, $event, $function) = @_;

	if (defined($function) && ref($function) eq 'CODE') {
		$self->{_EVENTCB}{$event} = $function;
	}
}

sub eventcallback {
	my ($self, %resp) = @_;

	my $callback;
	my $event = $resp{Event};

	return if (!$event);

	if (defined($self->{_EVENTCB}{$event})) {
		$callback = $self->{_EVENTCB}{$event};
	} elsif (defined($self->{_EVENTCB}{DEFAULT})) {
		$callback = $self->{_EVENTCB}{DEFAULT};
	} else {
		return;
	}

	return &{$callback}(%resp);
}

sub eventloop {
	my ($self) = @_;

	while (1) {
		$self->handleevent;
	}
}

sub eventpending {
	my ($self) = @_;
  return $self->{_SELECT}->can_read(0.001) || $self->{_READBUFFER} ne ""; 
}

sub handleevent {
	my ($self) = @_;

  my @res = $self->read_response;
  return undef unless @res;

	my %resp = map { splitresult($_); } @res;
	$self->eventcallback(%resp);

	return %resp;
}

sub action {
	my ($self, $command, $wanthash) = @_;

	return if (!$command);

	my $conn = $self->connfd || return;

  my $cstring = "Action: $command";
  if ($self->{_DEBUG}) {
    print("ACTION: $cstring");
  }
	syswrite($conn,  $cstring.$BLANK);

	my @resp = $self->read_response($conn);
  return undef unless @resp;

	if ($wanthash) {
		return map { splitresult($_) } @resp;
	} elsif (wantarray) {
		return @resp;
	} else {
		return $resp[0];
	}
}

sub command {
	my ($self, $command) = @_;

	return if (!$command);

	return $self->sendcommand('Action' => 'Command',
				  'Command' => $command, 1 );
}

sub disconnect {
	my ($self) = @_;

	my $conn = $self->connfd;

	my %resp = $self->sendcommand('Action' => 'Logoff');


	if ($resp{Response} eq 'Goodbye') {
		$self->{_CONNFD} = undef;
		$self->connected(0);
		return 1;
	}

	return 0;
}

sub splitresult {
	my ($res) = @_;
	my ($key, $val) = ('', '');

	$res =~ /^([^:]+):\ {0,1}([^\ ].*)$/;
	$key = $1 if ($1);
	$val = $2 if ($2);

	return ($key, $val);
}

1;
