The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
#  You may distribute under the terms of the GNU General Public License
#
#  (C) Paul Evans, 2008-2013 -- leonerd@leonerd.org.uk

package Circle::Configurable;

use strict;
use warnings;

use base qw( Circle::Commandable );

use Carp;

use Attribute::Storage qw( get_subattr get_subattrs );

#############################################
### Attribute handlers for setting_* subs ###
#############################################

my %setting_types = (
   str => {},

   int => {
      check => sub { m/^\d+$/ },
   },

   bool => {
      parse => sub {
         return 1 if lc $_ eq "true"  or lc $_ eq "on"  or $_ eq "1";
         return 0 if lc $_ eq "false" or lc $_ eq "off" or $_ eq "0";
         die;
      },
      print => sub { $_ ? "true" : "false" },
   },
);

sub Setting_description :ATTR(CODE)
{
   my $class = shift;
   my ( $text ) = @_;

   return $text;
}

sub Setting_type :ATTR(CODE)
{
   my $class = shift;
   my ( $typename ) = @_;

   exists $setting_types{$typename} or croak "Not a recognised type name '$typename'";

   return $setting_types{$typename};
}

sub Setting_default :ATTR(CODE)
{
   my $class = shift;
   my ( $value ) = @_;

   return $value;
}

sub _get_settings
{
   my $self = shift;

   my $class = ref $self || $self;

   my %settings;

   no strict 'refs';
   foreach my $name ( keys %{$class."::"} ) {
      ( my $settingname = $name ) =~ s/^setting_// or next;

      my $cv = $class->can( $name ) or next;
      $settings{$settingname} = get_subattrs( $cv );
   }

   return \%settings;
}

sub command_set
   : Command_description("Display or manipulate configuration settings")
   : Command_arg('setting?')
   : Command_arg('value?')
   : Command_opt('help=+',   desc => "Display help on setting(s)")
   : Command_opt('values=+', desc => "Display value of each setting")
{
   my $self = shift;
   my ( $setting, $newvalue, $opts, $cinv ) = @_;

   my $opt_help   = $opts->{help};
   my $opt_values = $opts->{values};

   if( !defined $setting ) {
      my $settings = $self->_get_settings;

      keys %$settings or $cinv->respond( "No settings exist" ), return;

      if( $opt_values ) {
         my @table;
         foreach my $settingname ( sort keys %$settings ) {
            my $curvalue = $self->can( "setting_$settingname" )->( $self );
            if( $setting->{type}->{print} ) {
               $curvalue = $setting->{type}->{print}->( $curvalue );
            }
            push @table, [ $settingname, defined $curvalue ? $curvalue : "" ];
         }

         $self->respond_table( \@table, colsep => ": ", headings => [ "Setting", "Value" ] );
      }
      else {
         my @table;
         foreach my $settingname ( sort keys %$settings ) {
            my $setting = $settings->{$settingname};

            push @table, [ $settingname, ( $setting->{Setting_description} || "[no description]" ) ];
         }

         $cinv->respond_table( \@table, colsep => " - ", headings => [ "Setting", "Description" ] );
      }

      return;
   }

   my $cv = $self->can( "setting_$setting" );
   if( !defined $cv ) {
      $cinv->responderr( "No such setting $setting" );
      return;
   }

   if( $opt_help ) {
      my $description = get_subattr( $cv, 'Setting_description' ) || "[no description]";
      $cinv->respond( "$setting - $description" );
      return;
   }

   my $type = get_subattr( $cv, 'Setting_type' );

   my $curvalue;
   if( defined $newvalue ) {
      if( $type->{check} ) {
         local $_ = $newvalue;
         $type->{check}->( $newvalue ) or
            $cinv->responderr( "'$newvalue' is not a valid value for $setting" ), return;
      }

      if( $type->{parse} ) {
         local $_ = $newvalue;
         eval { $newvalue = $type->{parse}->( $newvalue ); 1 } or
            $cinv->responderr( "'$newvalue' is not a valid value for $setting" ), return;
      }

      $curvalue = $cv->( $self, $newvalue );
   }
   else {
      $curvalue = $cv->( $self );
   }

   if( $type->{print} ) {
      local $_ = $curvalue;
      $curvalue = $type->{print}->( $curvalue );
   }

   if( defined $curvalue ) {
      $cinv->respond( "$setting: $curvalue" );
   }
   else {
      $cinv->respond( "$setting is not set" );
   }

   return;
}

sub get_configuration
{
   my $self = shift;

   my $ynode = YAML::Node->new({});
   $self->store_configuration( $ynode );

   return $ynode;
}

sub load_settings
{
   my $self = shift;
   my ( $ynode ) = @_;

   foreach my $setting ( keys %{ $self->_get_settings } ) {
      my $cv = $self->can( "setting_$setting" ) or croak "$self has no setting $setting";
      my $value = $ynode->{$setting};
      if( !defined $value and
          defined( my $default = get_subattr( $cv, "Setting_default" ) ) ) {
         $value = $default;
      }
      $cv->( $self, $value ) if defined $value;
   }
}

sub store_settings
{
   my $self = shift;
   my ( $ynode ) = @_;

   foreach my $setting ( keys %{ $self->_get_settings } ) {
      my $cv = $self->can( "setting_$setting" ) or croak "$self has no setting $setting";
      my $value = $cv->( $self );
      $ynode->{$setting} = $value if defined $value;
   }
}

0x55AA;