The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package IRC::Toolkit::ISupport;
{
  $IRC::Toolkit::ISupport::VERSION = '0.072000';
}
use 5.10.1;
use Carp 'confess';
use strictures 1;

use Scalar::Util 'blessed';

use IRC::Message::Object 'ircmsg';

use Exporter 'import';
our @EXPORT = 'parse_isupport';

my $parse = +{

  chanlimit => sub {
    my ($val) = @_;
    my @chunks = split /,/, $val;

    my $ref = {};
    for my $chunk (@chunks) {
      my ($prefixed, $num) = split /:/, $chunk;
      my @prefixes = split '', $prefixed;
      for my $pfx (@prefixes) {
        $ref->{$pfx} = $num
      }
    }

    $ref
  },

  chanmodes => sub {
    my ($val) = @_;
    my ($list, $always, $whenset, $bool) = split /,/, $val;
    +{
      list    => [ split '', $list ],
      always  => [ split '', $always ],
      whenset => [ split '', $whenset ],
      bool    => [ split '', $bool ],
    }
  },

  chantypes => sub {
    my ($val) = @_;
    +{  map {; $_ => 1 } split '', $val }
  },

  maxlist => sub {
    my ($val) = @_;
    my @chunks = split /,/, $val;

    my $ref = {};
    for my $chunk (@chunks) {
      my ($modes, $num) = split /:/, $chunk;
      my @splitm = split '', $modes;
      for my $mode (@splitm) {
        $ref->{$mode} = $num
      }
    }

    $ref
  },

  prefix => sub {
    my ($val) = @_;
    my ($modes, $prefixes) = $val =~ /\(([^)]+)\)(.+)/;
    return +{} unless $modes and $prefixes;

    my @modes = split '', $modes;
    my @pfxs  = split '', $prefixes;
    unless (@modes == @pfxs) {
      warn "modes/prefixes do not appear to match: $modes $prefixes";
      return +{}
    }

    my $ref;
    for my $mode (@modes) {
      $ref->{$mode} = shift @pfxs
    }
    $ref
  },

  statusmsg => sub {
    my ($val) = @_;
    +{ map {; $_ => 1 } split '', $val }
  },

};


sub _isupport_hash {
  my ($obj) = @_;
  my %cur;
  confess "No object passed or no params to process"
    unless defined $obj and @{ $obj->params };
  ## First arg should be the target.
  ## Last is 'are supported by ...'
  my %split = map {;
    my ($key, $val) = split /=/, $_, 2;
    ( lc($key), ($val // '0 but true') )
  } @{ $obj->params }[1 .. ($#{ $obj->params } - 1) ];

  unless (keys %split) {
    warn "Appear to have been passed valid IRC, but not an ISUPPORT string";
    return +{}
  }

  for my $param (map {; lc $_ } keys %split) {
    if (defined $parse->{$param}) {
      $cur{$param} = $parse->{$param}->($split{$param})
    } else {
      $cur{$param} = $split{$param} 
    }
  }

  \%cur
}

sub _isupport_hash_to_obj {
  my ($isupport_hash) = @_;
  IRC::Toolkit::ISupport::Obj->new($isupport_hash)
}

sub parse_isupport {
  my @items = map {;
    blessed $_ ? $_ : ircmsg(raw_line => $_)
  } @_;

  confess 
    'Expected a list of raw IRC lines or IRC::Message::Object instances'
    unless @items;

  my %cur;
  ITEM: for my $item (@items) {
    if ($item->isa('IRC::Message::Object')) {
      my $piece = _isupport_hash($item);
      @cur{keys %$piece} = values %$piece;
      next ITEM
    } else {
      confess "expected an IRC::Message::Object but got $item"
    }
  }

  _isupport_hash_to_obj(\%cur);
}


{ package
  IRC::Toolkit::_ISchanmodes;
  use Carp 'confess';
  use strictures 1;

  sub new {
    my ($cls, $self) = @_;
    confess 'Expected a HASH' unless ref $self eq 'HASH';
    bless $self, $cls 
  }

  sub list    { $_[0]->{list} }
  sub always  { $_[0]->{always} }
  sub whenset { $_[0]->{whenset} }
  sub bool    { $_[0]->{bool} }
}

{ package
  IRC::Toolkit::ISupport::Obj;

  use Carp 'confess';
  use strictures 1;
  use Scalar::Util 'blessed';

  sub new {
    my ($cls, $self) = @_;
    confess "Expected a HASH from _isupport_hash"
      unless ref $self eq 'HASH';
    bless $self, $cls
  }

  sub add {
    my ($self, @items) = @_;
    my %cur = %$self;
    for my $item (@items) {
      confess "Expected HASH or ISupport::Obj, got $item"
        unless blessed $item
        or ref $item eq 'HASH';

      @cur{keys %$item} = values %$item;
    }
    (ref $self || $self)->new(\%cur)
  }

  sub chanlimit {
    my ($self, $val) = @_;
    return ($self->{chanlimit} // {}) unless defined $val;
    $self->{chanlimit}->{$val}
  }

  sub chanmodes {
    my ($self) = @_;
    return unless $self->{chanmodes};
    unless (blessed $self->{chanmodes}) {
      $self->{chanmodes} = 
        IRC::Toolkit::_ISchanmodes->new($self->{chanmodes})
    }
    $self->{chanmodes}
  }

  sub chantypes {
    my ($self, $val) = @_;
    return ($self->{chantypes} // {}) unless defined $val;
    $self->{chantypes}->{$val}
  }

  sub maxlist {
    my ($self, $val) = @_;
    return ($self->{maxlist} // {}) unless defined $val;
    $self->{maxlist}->{$val}
  }

  sub prefix {
    my ($self, $val) = @_;
    return ($self->{prefix} // {}) unless defined $val;
    $self->{prefix}->{$val}
  }

  sub statusmsg {
    my ($self, $val) = @_;
    return ($self->{statusmsg} // {}) unless defined $val;
    $self->{statusmsg}->{$val}
  }

  ## Everything else is bool / int / str we can't parse
  our $AUTOLOAD;
  sub AUTOLOAD {
    my ($self, $val) = @_;
    my $subname = (split /::/, $AUTOLOAD)[-1];
    return if index($subname, 'DESTROY') == 0;
    $self->{$subname}
  }

}


1;

=pod

=head1 NAME

IRC::Toolkit::ISupport - IRC ISUPPORT parser

=head1 SYNOPSIS

  use IRC::Toolkit::ISupport;
  my $isupport = parse_isupport(@lines);

  ## Get the MODES= value
  my $maxmodes = $isupport->modes;

  ## Get the PREFIX= char for mode 'o'
  my $prefix_for_o = $isupport->prefix('o');

  ## ... etc ...

=head1 DESCRIPTION

An ISUPPORT (IRC numeric 005) parser that accepts either raw IRC lines or
L<IRC::Message::Object> instances and produces struct-like objects with some
special magic for parsing known ISUPPORT types.

See L<http://www.irc.org/tech_docs/005.html>

=head2 parse_isupport

Takes a list of raw IRC lines or L<IRC::Message::Object> instances and
produces ISupport objects.

Keys not listed here will return their raw value (or '0 but true' for boolean
values).

The following known keys are parsed to provide a nicer interface:

=head3 chanlimit

If passed a channel prefix character, returns the CHANLIMIT= value for that
prefix.

Without any arguments, returns a HASH mapping channel prefixes to their
respective CHANLIMIT= value.

=head3 chanmodes

The four mode sets described by a compliant CHANMODES= declaration are list
modes, modes that always take a parameter, modes that take a parameter only
when they are set, and boolean-type 'flag' modes, respectively:

  CHANMODES=LIST,ALWAYS,WHENSET,BOOL

You can retrieve ARRAYs containing lists of modes belonging to each set:

  my @listmodes = @{ $isupport->chanmodes->list };
  my $always  = $isupport->chanmodes->always;
  my $whenset = $isupport->chanmodes->whenset;
  my $boolean = $isupport->chanmodes->bool;

=head3 chantypes

Without any arguments, returns a HASH whose keys are the allowable channel
prefixes.

If given a channel prefix, returns boolean true if the channel prefix is
allowed per CHANTYPES.

=head3 maxlist

Without any arguments, returns a HASH mapping list-type modes (see
L</chanmodes>) to their respective numeric limit.

If given a list-type mode, returns the limit for that list.

=head3 prefix

Without any arguments, returns a HASH mapping status modes to their respective
prefixes.

If given a status modes, returns the prefix belonging to that mode.

=head3 statusmsg

Without any arguments, returns a HASH whose keys are the valid message target 
status prefixes.

If given a status prefix, returns boolean true if the prefix is listed in
STATUSMSG.

=head1 MISSING

... typically because I'm not sure if there's authoritative documentation or
where it might be found ...

=over

=item *

ELIST

=item *

EXTBAN

=item *

TARGMAX

=back

=head1 AUTHOR

Jon Portnoy <avenj@cobaltirc.org>

=cut