package Text::Autoformat::Hang;
$Text::Autoformat::Hang::VERSION = '1.75';
use 5.006;
use strict;
use warnings;

# ROMAN NUMERALS

sub inv($@) { my ($k, %inv)=shift; for(0..$#_) {$inv{$_[$_]}=$_*$k} %inv }
my @unit= ( "" , qw ( I II III IV V VI VII VIII IX ));
my @ten = ( "" , qw ( X XX XXX XL L LX LXX LXXX XC ));
my @hund= ( "" , qw ( C CC CCC CD D DC DCC DCCC CM ));
my @thou= ( "" , qw ( M MM MMM ));
my %rval= (inv(1,@unit),inv(10,@ten),inv(100,@hund),inv(1000,@thou));
my $rbpat= join ")(",join("|",reverse @thou), join("|",reverse @hund), join("|",reverse @ten), join("|",reverse @unit);
my $rpat= join ")(?:",join("|",reverse @thou), join("|",reverse @hund), join("|",reverse @ten), join("|",reverse @unit);
my $rom = qq/(?:(?=[MDCLXVI])(?:$rpat))/;

my $abbrev = join '|', qw{ etc[.]   pp[.]   ph[.]?d[.] },
                       "(?!$rom)(?:[A-Z][A-Za-z]+[.])+",
                       '(?:[A-Z][.])(?:[A-Z][.])+';

sub fromRoman($)
{
    return 0 unless $_[0] =~ /^.*?($rbpat).*$/i;
    return $rval{uc $1} + $rval{uc $2} + $rval{uc $3} + $rval{uc $4};
}

sub toRoman($$)
{
    my ($num,$example) = @_;
    return '' unless $num =~ /^([0-3]??)(\d??)(\d??)(\d)$/;
    my $roman = $thou[$1||0] . $hund[$2||0] . $ten[$3||0] . $unit[$4||0];
    return $example=~/[A-Z]/ ? uc $roman : lc $roman;
}

# BITS OF A NUMERIC VALUE

my $num = q/(?:[0-9]{1,3}\b(?!:[0-9][0-9]\b))/;     # Ignore 8:20 etc.
my $let = q/[A-Za-z]/;
my $pbr = q/[[(<]/;
my $sbr = q/])>/;
my $ows = q/[ \t]*/;
my %close = ( '[' => ']', '(' => ')', '<' => '>', "" => '' );

my $hangPS      = qq{(?i:ps:|(?:p\\.?)+s\\b\\.?(?:[ \\t]*:)?)};
my $hangNB      = qq{(?i:n\\.?b\\.?(?:[ \\t]*:)?)};
my $hangword    = qq{(?:(?:Note)[ \\t]*:)};
my $hangbullet  = qq{[*.+-]};
my $hang        = qq{(?:(?i)(?:$hangNB|$hangword|$hangbullet)(?=[ \t]))};

# IMPLEMENTATION

sub new { 
    my ($class, $orig, $lists_mode) = @_;
    return Text::Autoformat::NullHang->new() if !$lists_mode;

    my $origlen = length $orig;
    my @vals;
    if ($_[1] =~ s#\A($hangPS)##) {
        @vals = { type => 'ps', val => $1 }
    }
    elsif ($lists_mode =~ /1|bullet/i && $_[1] =~ s#\A($hang)##) {
        @vals = { type => 'bul', val => $1 }
    }
    elsif ($_[1] =~ m#\A\([^\s)]+\s#) {
        @vals = ();
    }
    else {
        no warnings "all";
        my $cut;
        while (length $_[1]) {
            last if $_[1] =~ m#\A($ows)($abbrev)#
                 && (length $1 || !@vals);  # ws-separated or first

            last if $_[1] =~ m{\A $ows $pbr [^$sbr \t]* \s}xms;

            $cut = $origlen - length $_[1];
            my $pre = $_[1] =~ s#\A($ows$pbr$ows)## ? $1 : "";
            my $val
                = ($lists_mode =~ /1|number/i && $_[1] =~ s#\A($num)##)
                        ? { type=>'num', val=>$1 }
                : ($lists_mode =~ /1|roman/i && $_[1] =~ s#\A($rom)\b##i)
                        ? { type=>'rom', val=>$1, nval=>fromRoman($1) }
                : ($lists_mode =~ /1|alpha/i && $_[1] =~ s#\A($let(?!$let))##i)
                        ? { type=>'let', val=>$1 }
                :         { val => "", type => "" };
            $_[1] = $pre.$_[1] and last unless length $val->{val};
            $val->{post} = $pre && $_[1] =~ s#\A($ows()[.:/]?[$close{$pre}][.:/]?)## && $1
                             || $_[1] =~ s#\A($ows()[$sbr.:/])## && $1
                             || "";
            $val->{pre}  = $pre;
            $val->{cut}  = $cut;
            push @vals, $val;
        }
        while (@vals && !$vals[-1]{post}) {
            $_[1] = substr($orig,pop(@vals)->{cut});
        }
    }

    # check for orphaned years or unlikely Roman numerals...
    if (@vals==1 && defined $vals[0]->{post} && $vals[0]->{post} =~ /[\.>)]/) {
        my $v = $vals[0];
        if ($v->{type} eq 'num' && $v->{val} >= 1000) {
            $_[1] = substr($orig,pop(@vals)->{cut});
        }
    }

    return Text::Autoformat::NullHang->new if !@vals;
    bless \@vals, $class;
} 

sub incr {
    no warnings "all";
    my ($self, $prev, $prevsig) = @_;
    my $level;
    # check compatibility

    return unless $prev && !$prev->empty;

    for $level (0..(@$self<@$prev ? $#$self : $#$prev)) {
        if ($self->[$level]{type} ne $prev->[$level]{type}) {
            return if @$self<=@$prev;   # no incr if going up
            $prev = $prevsig;
            last;
        }
    }
    return unless $prev && !$prev->empty;
    if ($self->[0]{type} eq 'ps') {
        my $count = 1 + $prev->[0]{val} =~ s/(p[.]?)/$1/gi;
        $prev->[0]{val} =~ /^(p[.]?).*(s[.]?[:]?)/;
        $self->[0]{val} = $1  x $count . $2;
    }
    elsif ($self->[0]{type} eq 'bul') {
        # do nothing
    }
    elsif (@$self>@$prev) { # going down level(s)
        for $level (0..$#$prev) {
                @{$self->[$level]}{'val','nval'} = @{$prev->[$level]}{'val','nval'};
        }
        for $level (@$prev..$#$self) {
                _reset($self->[$level]);
        }
    }
    else    # same level or going up
    {
        for $level (0..$#$self) {
            @{$self->[$level]}{'val','nval'} = @{$prev->[$level]}{'val','nval'};
        }
        _incr($self->[-1])
    }
}

sub _incr {
    no warnings "all";
    if ($_[0]{type} eq 'rom') {
        $_[0]{val} = toRoman(++$_[0]{nval},$_[0]{val});
    }
    else {
        $_[0]{val}++ unless $_[0]{type} eq 'let' && $_[0]{val}=~/Z/i;
    }
}

sub _reset {
    no warnings "all";
    if ($_[0]{type} eq 'rom') {
        $_[0]{val} = toRoman($_[0]{nval}=1,$_[0]{val});
    }
    elsif ($_[0]{type} eq 'let') {
        $_[0]{val} = $_[0]{val} =~ /[A-Z]/ ? 'A' : 'a';
    }
    else {
        $_[0]{val} = 1;
    }
}

sub stringify {
    my ($self) = @_;
    my ($str, $level) = ("");
    for $level (@$self) {
        no warnings "all";
        $str .= join "", @{$level}{'pre','val','post'};
    }
    return $str;
} 

sub val {
    my ($self, $i) = @_;
    return $self->[$i]{val};
}

sub fields { return scalar @{$_[0]} }

sub field {
    my ($self, $i, $newval) = @_;
    $self->[$i]{type} = $newval if @_>2;
    return $self->[$i]{type};
}

sub signature {
    no warnings "all";
    my ($self) = @_;
    my ($str, $level) = ("");
    for $level (@$self) {
        $level->{type} ||= "";
        $str .= join "", $level->{pre},
                         ($level->{type} =~ /rom|let/ ? "romlet" : $level->{type}),
                         $level->{post};
    }
    return $str;
} 

sub length {
    length $_[0]->stringify
}

sub empty { 0 }

1;