#  You may distribute under the terms of either the GNU General Public License
#  or the Artistic License (the same terms as Perl itself)
#
#  (C) Paul Evans, 2015 -- leonerd@leonerd.org.uk

package App::MatrixTool;

use strict;
use warnings;

our $VERSION = '0.01';

use Getopt::Long qw( GetOptionsFromArray );
use Future;
use MIME::Base64 qw( encode_base64 );
use Module::Pluggable::Object;
use Module::Runtime qw( use_package_optimistically );
use Scalar::Util qw( blessed );
use Socket qw( getnameinfo AF_INET AF_INET6 AF_UNSPEC NI_NUMERICHOST NI_NUMERICSERV );

my $opt_parser = Getopt::Long::Parser->new(
   config => [qw( require_order )],
);

=head1 NAME

C<App::MatrixTool> - commands to interact with a Matrix home-server

=head1 SYNOPSIS

Usually this would be used via the F<matrixtool> command

 $ matrixtool server-key matrix.org

=head1 DESCRIPTION

Provides the base class and basic level support for commands that interact
with a Matrix home-server. See individual command modules, found under the
C<App::MatrixTool::Command::> namespace, for details on specific commands.

=cut

sub new
{
   my $class = shift;
   return bless { @_ }, $class;
}

sub sock_family
{
   my $self = shift;
   return AF_INET if $self->{inet4};
   return AF_INET6 if $self->{inet6};
   return AF_UNSPEC;
}

sub run
{
   my $self = shift;
   my @args = @_;

   my %global_opts;
   $opt_parser->getoptionsfromarray( \@args,
      'inet4|4' => \$global_opts{inet4},
      'inet6|6' => \$global_opts{inet6},
      'print-request'  => \$global_opts{print_request},
      'print-response' => \$global_opts{print_response},
   ) or return 1;

   my $cmd = @args ? shift @args : "help";
   # Allow hyphens in command names
   $cmd =~ s/-/_/g;

   my $pkg = "App::MatrixTool::Command::$cmd";
   use_package_optimistically( $pkg );

   $pkg->can( "new" ) or
      return $self->error( "No such command '$cmd'" );

   if( $pkg->can( "OPTIONS" ) ) {
      my %optdefs = $pkg->OPTIONS;
      my %optvalues;

      GetOptionsFromArray( \@args,
         map { $_ => \$optvalues{ $optdefs{$_} } } keys %optdefs,
      ) or exit 1;

      unshift @args, \%optvalues;
   }

   my $ret = $pkg->new( %global_opts )->run( @args );
   $ret = $ret->get if blessed $ret and $ret->isa( "Future" );
   $ret //= 0;

   return $ret;
}

sub output
{
   my $self = shift;
   print @_, "\n";

   return 0;
}

# Some nicer-formatted outputs for terminals
sub output_ok
{
   my $self = shift;
   $self->output( "\e[32m", "[OK]", "\e[m", " ", @_ );
}

sub output_info
{
   my $self = shift;
   $self->output( "\e[36m", "[INFO]", "\e[m", " ", @_ );
}

sub output_warn
{
   my $self = shift;
   $self->output( "\e[33m", "[WARN]", "\e[m", " ", @_ );
}

sub output_fail
{
   my $self = shift;
   $self->output( "\e[31m", "[FAIL]", "\e[m", " ", @_ );
}

sub format_binary
{
   my $self = shift;
   my ( $bin ) = @_;

   # TODO: A global option to pick the format here
   return "base64::" . do { local $_ = encode_base64( $bin, "" ); s/=+$//; $_ };
}

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

   return "[$host]:$port" if $host =~ m/:/; # IPv6
   return "$host:$port";
}

sub format_addr
{
   my $self = shift;
   my ( $addr ) = @_;
   my ( $err, $host, $port ) = getnameinfo( $addr, NI_NUMERICHOST|NI_NUMERICSERV );
   $err and die $err;

   return $self->format_hostport( $host, $port );
}

sub error
{
   my $self = shift;
   print STDERR @_, "\n";

   return 1;
}

## Command support

sub http_client
{
   my $self = shift;

   return $self->{http_client} ||= do {
      require App::MatrixTool::HTTPClient;
      App::MatrixTool::HTTPClient->new(
         family => $self->sock_family,
         map { $_ => $self->{$_} } qw( print_request print_response ),
      );
   };
}

sub key_store_path { "$ENV{HOME}/.matrix/server-keys" }

sub key_store
{
   my $self = shift;

   return $self->{key_store} ||= do {
      require App::MatrixTool::KeyStore;
      App::MatrixTool::KeyStore->new(
         path => $self->key_store_path
      );
   };
}

## Builtin commands

package
   App::MatrixTool::Command::help;
use base qw( App::MatrixTool );

use List::Util qw( max );

use constant DESCRIPTION => "Display help information about commands";

sub commands
{
   my $mp = Module::Pluggable::Object->new(
      require => 1,
      search_path => [ "App::MatrixTool::Command" ],
   );

   my @commands;

   foreach my $module ( sort $mp->plugins ) {
      my $cmd = $module;
      $cmd =~ s/^App::MatrixTool::Command:://;
      $cmd =~ s/_/-/g;

      push @commands, [ $cmd, $module->DESCRIPTION ];
   }

   return @commands;
}

sub run
{
   my $self = shift;

   $self->output( <<'EOF' );
matrixtool [<global options...>] <command> [<command-options...>]

Global options:
   -4 --inet4             Use only IPv4
   -6 --inet6             Use only IPv6
      --print-request     Print sent HTTP requests in full
      --print-response    Print received HTTP responses in full
EOF

   my @commands = $self->commands;

   my $namelen = max map { length $_->[0] } @commands;

   $self->output( "Commands:\n" .
      join "\n", map { sprintf "  %-*s    %s", $namelen, @$_ } @commands
   );
}

=head1 AUTHOR

Paul Evans <leonerd@leonerd.org.uk>

=cut

0x55AA;
