package Inline::denter;

use strict;
use Carp;

sub new {
    my $class = shift;
    bless {width => 4,
           comma => " : ",
           level => 0,
           tabwidth => 8,
          }, $class;
}

# Prevent a taint exception being thrown by AutoLoader.pm.
# Serves no other purpose.
sub DESTROY {
}

sub undent {
    local $/ = "\n";
    my ($o, $text) = @_;
    my ($comma) = $o->{comma};
    my $package = caller;
    $package = caller(1) if $package eq 'Inline::denter';
    %{$o->{xref}} = ();
    @{$o->{objects}} = ();
    @{$o->{context}} = ();
    my $glob = '';
    chomp $text;
    @{$o->{lines}} = split $/, $text;
    $o->{level} = 0;
    $o->{line} ||= 1;
    $o->_setup_line;
    while (not $o->{done}) {
        if ($o->{level} == 0 and
            $o->{content} =~ /^(\w+)\s*$comma\s*(.*)$/) {
            $o->{content} = $2;
            no strict 'refs';
            push @{$o->{objects}}, "$1";
        }
        push @{$o->{objects}}, $o->_undent_data;
    }
    return @{$o->{objects}};
}

sub _undent_data {
    my $o = shift;
    my ($obj, $class) = ('', '');
    my @refs;
    my %refs;
    while ($o->{content} =~ s/^\\(?:\((\w+)\))?((\%|\@|\$|\\).*)/$2/) {
        push @refs, $1;
        $refs{$1} = scalar @refs;
    }
    if ($o->{content} =~ /^([\%\@\$])
                          (\w(?:\w|::)*)?
                          \s*$/x
       ) {
        my $foo;
        $obj = ($1 eq '%') ? {} : ($1 eq '@') ? [] : \$foo;
        $class = $2 || '';
        if ($1 eq '%') {
            %$obj = $o->_undent_hash;
        }
        elsif ($1 eq '@') {
            @$obj = $o->_undent_array;
        }
        else {
            $$obj = $o->_undent_scalar;
        }
        bless $obj, $class if length $class;
    }
    elsif ($o->{content} =~ /^\?\s*$/) {
        $obj = $o->_undent_undef;
    }
    else {
        $obj = $o->_undent_value;
    }
    while (@refs) {
        my $ref = pop @refs;
        my $copy = $obj;
        $obj = \ $copy;
        $o->{xref}{$ref} = $obj if $ref;
    }
    return $obj;
}

sub _undent_value {
    my $o = shift;
    my $value = '';
    if ($o->{content} =~ /^\<\<(\w+)(\-?)\s*$/) {
        my ($marker, $chomp) = ($1, $2);
        my $line = $o->{line};
        $o->_next_line;
        while (not $o->{done} and
               $o->{lines}[0] ne $marker) {
            $value .= $o->{lines}[0] . "\n";
            $o->_next_line;
        }
        croak M03_no_value_end_marker($marker, $line) if $o->{done};
        chomp $value if $chomp;
    }
    elsif ($o->{content} =~ /^\"/) {
        croak $o->M04_mismatched_quotes unless $o->{content} =~ /^\".*\"\s*$/;
        ($value = $o->{content}) =~ s/^\"|\"\s*$//g;
    }
    else {
        $value = $o->{content};
    }
    $o->_next_line;
    $o->_setup_line;
    return $value;
}

sub _undent_hash {
    my @values;
    my $o = shift;
    my $level = $o->{level} + 1;
    $o->_next_line;
    $o->_setup_line;
    while ($o->{level} == $level) {
        my ($key, $value) = split $o->{comma}, $o->{content};
        croak $o->M05_invalid_key_value unless (defined $key and defined $value);
        $o->{content} = $value;
        push @values, $o->_get_key($key), $o->_undent_data;;
    }
    croak $o->M06_invalid_indent_level if $o->{level} > $level;
    return @values;
}

sub _get_key {
    my ($o, $key) = @_;
    return $key unless $key =~ /^\<\<(\w+)(\-?)/;
    my ($marker, $chomp) = ($1, $2);
    $key = '';
    my $line = $o->{line};
    $o->_next_line;
    while (not $o->{done} and
           $o->{lines}[0] ne $marker) {
        $key .= $o->{lines}[0] . "\n";
        $o->_next_line;
    }
    croak M02_no_key_end_marker($marker, $line) if $o->{done};
    chomp $key if $chomp;
    $o->_next_line;
    $o->_setup_line;
    return $key;
}

sub _undent_array {
    my @values;
    my $o = shift;
    my $level = $o->{level} + 1;
    $o->_next_line;
    $o->_setup_line;
    while ($o->{level} == $level) {
        push @values, $o->_undent_data;
    }
    croak $o->M06_invalid_indent_level if $o->{level} > $level;
    return @values;
}

sub _undent_scalar {
    my $values;
    my $o = shift;
    my $level = $o->{level} + 1;
    $o->_next_line;
    $o->_setup_line;
    croak $o->M06_invalid_indent_level if $o->{level} != $level;
    croak $o->M07_invalid_scalar_value if $o->{content} =~ /^[\%\@\$\\]/;
    return $o->_undent_undef if $o->{content} =~ /^\?/;
    return $o->_undent_value;
}

sub _undent_undef {
    my $o = shift;
    $o->_next_line;
    $o->_setup_line;
    return undef;
}

sub _next_line {
    my $o = shift;
    $o->{done}++, $o->{level} = -1, return unless @{$o->{lines}};
    local $_ = shift @{$o->{lines}};
    $o->{line}++;
}

sub _setup_line {
    my $o = shift;
    $o->{done}++, $o->{level} = -1, return unless @{$o->{lines}};
    my ($width, $tabwidth) = @{$o}{qw(width tabwidth)};
    while (1) {
        local $_ = $o->{lines}[0];
        # expand tabs in leading whitespace;
        $o->next_line, next if /^(\s*$|\#)/; # skip comments and blank lines
        while (s{^( *)(\t+)}
               {' ' x (length($1) + length($2) * $tabwidth -
                       length($1) % $tabwidth)}e){}
        croak $o->M01_invalid_indent_width unless /^(( {$width})*)(\S.*)$/;
        $o->{level} = length($1) / $width;
        $o->{content} = $3;
        last;
    }
}

sub indent {
    my $o = shift;
    my $package = caller;
    $package = caller(1) if $package eq 'Inline::denter';
    my $stream = '';
    $o->{key} = '';
    while (@_) {
        local $_ = shift;
        $stream .= $o->indent_name($_, shift), next
          if (/^\*$package\::\w+$/);
        $stream .= $o->indent_data($_);
    }
    return $stream;
}

sub indent_data {
    my $o = shift;
    local $_ = shift;
    return $o->indent_undef($_)
      if not defined;
    return $o->indent_value($_)
      if (not ref);
    return $o->indent_hash($_)
      if (ref eq 'HASH' and not /=/ or /=HASH/);
    return $o->indent_array($_)
      if (ref eq 'ARRAY' and not /=/ or /=ARRAY/);
    return $o->indent_scalar($_)
      if (ref eq 'SCALAR' and not /=/ or /=SCALAR/);
    return $o->indent_ref($_)
      if (ref eq 'REF');
    return "$_\n";
}

sub indent_value {
    my ($o, $data) = @_;
    my $stream;
    if ($data =~ /\n/) {
        my $marker = 'EOV';
        $marker++ while $data =~ /^$marker$/m;
        my $chomp = ($data =~ s/\n\Z//) ? '' : '-';
        $stream = "<<$marker$chomp\n";
        $stream .= $o->{key}, $o->{key} = '' if $o->{key};
        $stream .= "$data\n$marker\n";
    }
    elsif ($data =~ /^[\s\%\@\$\\?\"]|\s$/ or
           $data =~ /\Q$o->{comma}\E/ or
           $data =~ /[\x00-\x1f]/ or
           $data eq '') {
        $stream = qq{"$data"\n};
        $stream .= $o->{key}, $o->{key} = '' if $o->{key};
    }
    else {
        $stream = "$data\n";
        $stream .= $o->{key}, $o->{key} = '' if $o->{key};
    }
    return $stream;
}

sub indent_hash {
    my ($o, $data) = @_;
    my $stream = $o->_print_ref($data, '%', 'HASH');
    return $$stream if ref $stream;
    my $indent = ++$o->{level} * $o->{width};
    for my $key (sort keys %$data) {
        my $key_out = $key;
        if ($key =~ /\n/ or
            $key =~ /\Q$o->{comma}\E/) {
            my $marker = 'EOK';
            $marker++ while $key =~ /^$marker$/m;
            my $chomp = (($o->{key} = $key) =~ s/\n\Z//m) ? '' : '-';
            $o->{key} .= "\n$marker\n";
            $key_out = "<<$marker$chomp";
        }
        elsif ($data =~ /^[\s\%\@\$\\?\"]|\s$/) {
            $key_out = qq{"$key"};
        }
        $stream .= ' ' x $indent . $key_out . $o->{comma};
        $stream .= $o->indent_data($data->{$key});
    }
    $o->{level}--;
    return $stream;
}

sub indent_array {
    my ($o, $data) = @_;
    my $stream = $o->_print_ref($data, '@', 'ARRAY');
    return $$stream if ref $stream;
    my $indent = ++$o->{level} * $o->{width};
    for my $datum (@$data) {
        $stream .= ' ' x $indent;
        $stream .= $o->indent_data($datum);
    }
    $o->{level}--;
    return $stream;
}

sub indent_scalar {
    my ($o, $data) = @_;
    my $stream = $o->_print_ref($data, q{$}, 'SCALAR');
    return $$stream if ref $stream;
    my $indent = ($o->{level} + 1) * $o->{width};
    $stream .= ' ' x $indent;
    $stream .= $o->indent_data($$data);
    return $stream;
}

sub indent_ref {
    my ($o, $data) = @_;
    my $stream = $o->_print_ref($data, '\\', 'SCALAR');
    return $$stream if ref $stream;
    chomp $stream;
    return $stream . $o->indent_data($$data);
}

sub indent_undef {
    my ($o, $data) = @_;
    my $stream = "?\n";
    $stream .= $o->{key}, $o->{key} = '' if $o->{key};
    return $stream;
}

sub indent_name {
    my ($o, $name, $value) = @_;
    $name =~ s/^.*:://;
    my $stream = $name . $o->{comma};
    $stream .= $o->indent_data($value);
    return $stream;
}

sub _print_ref {
    my ($o, $data, $symbol, $type) = @_;
    $data =~ /^(([\w:]+)=)?$type\(0x([0-9a-f]+)\)$/
      or croak "Invalid reference: $data\n";
    my $stream = $symbol;
    $stream .= $2 if defined $2;
    $o->{xref}{$3}++;
    croak "Inline::denter does not handle duplicate references"
      if $o->{xref}{$3} > 1;
    $stream .= "\n";
    $stream .= $o->{key}, $o->{key} = '' if $o->{key};
    return $stream;
}

# Undent error messages
sub M01_invalid_indent_width {
    my $o = shift;
    "Invalid indent width detected at line $o->{line}\n";
}

sub M02_no_key_end_marker {
    my ($marker, $line) = @_;
    "No terminating marker '$marker' found for key at line $line\n";
}

sub M03_no_value_end_marker {
    my ($marker, $line) = @_;
    "No terminating marker '$marker' found for value at line $line\n";
}

sub M04_mismatched_quotes {
    my $o = shift;
    "Mismatched double quotes for value at line $o->{line}\n";
}

sub M05_invalid_key_value {
    my $o = shift;
    "Missing or invalid hash key/value pair at $o->{line}\n";
}

sub M06_invalid_indent_level {
    my $o = shift;
    "Invalid indentation level at $o->{line}\n";
}

sub M07_invalid_scalar_value {
    my $o = shift;
    "Invalid value for scalar ref context at $o->{line}\n";
}

1;
__END__
