package Data::Difflet;
use strict;
use warnings FATAL => 'recursion';
use 5.008008;
our $VERSION = '0.06';
use Term::ANSIColor;
use Data::Dumper;

our $LEVEL;
our $BUFFER;

sub new {
    my $class = shift;
    bless {
        inserted_color => 'green',
        deleted_color => 'red',
        updated_color => 'blue',
        comment_color => 'cyan',
        indent => 2,
    }, $class;
}

sub _f($) { die "Do not call directly"; }

sub ddf {
    my $self = shift;
    @_==1 or die;

    local $Data::Dumper::Terse = 1;
    local $Data::Dumper::Indent = 0;
    Dumper(@_);
}

sub compare {
    my $self = shift;
    local $LEVEL = 0;
    local $BUFFER = '';
    no warnings 'redefine';
    local *_f = sub($) { $self->ddf(@_) };
    local $Term::ANSIColor::EACHLINE = "\n";
    $self->_compare(@_);
    return $BUFFER;
}

# TODO: recursion detection
sub _compare {
    my ($self, $a, $b) = @_;
    if (ref $a eq 'HASH') { # dump hash
        if (ref $b eq 'HASH') {
            $self->_print("{\n");
            {
                local $LEVEL = $LEVEL + 1;
                for my $key (sort keys %$a) {
                    if (exists $b->{$key}) {
                        if ($self->ddf($b->{$key}) eq $self->ddf($a->{$key})) {
                            $self->_print("%s => %s,\n", $self->ddf($key), $self->ddf($a->{$key}));
                        } else {
                            if (ref($a->{$key}) or ref($b->{$key})) {
                                $self->_print("%s => ", _f($key));
                                local $LEVEL = $LEVEL + 1;
                                $self->_compare($a->{$key}, $b->{$key});
                                $self->_print(",\n");
                            } else {
                                $self->_updated("%s => %s,", _f($key), _f($a->{$key}));
                                $self->_comment(" # != %s,\n", _f($b->{$key}));
                            }
                        }
                    } else {
                        $self->_inserted("%s => %s,\n", $self->ddf($key), $self->ddf($a->{$key}));
                    }
                }
                for my $key (sort keys %$b) {
                    next if exists $a->{$key};
                    $self->_deleted("%s => %s,\n", $self->ddf($key), $self->ddf($b->{$key}));
                }
            }
            $self->_print("}\n");
            return;
        } else {
            $self->_inserted("%s\n", $self->ddf($a));
            $self->_deleted("%s\n",  $self->ddf($b));
        }
    } elsif (ref $a eq 'ARRAY') {
        if (ref $b eq 'ARRAY') {
            $self->_print("[\n");
            {
                local $LEVEL = $LEVEL + 1;
                my $alen = 0+@$a;
                my $blen = 0+@$b;
                my $i = 0;
                while (1) {
                    if ($i<$alen && $i<$blen) { # both
                        if (_f($a->[$i]) eq _f($b->[$i])) {
                            $self->_print("%s,\n", _f($a->[$i]));
                        } else {
                            if (ref($a->[$i]) or ref($b->[$i])) {
                                local $LEVEL = $LEVEL + 1;
                                $self->_compare($a->[$i], $b->[$i]);
                            } else {
                                $self->_updated("%s,", $a->[$i]);
                                $self->_comment(" # != %s\n", $b->[$i]);
                            }
                        }
                    } elsif ($i<$alen) {
                        $self->_inserted("%s,\n", _f $a->[$i]);
                    } elsif ($i<$blen) {
                        $self->_deleted("%s,\n", _f $b->[$i]);
                    } else {
                        last;
                    }
                    ++$i;
                }
            }
            $self->_print("]\n");
        } else {
            $self->_inserted("%s\n", $self->ddf($a));
            $self->_deleted("%s\n",  $self->ddf($b));
        }
    } else {
        if ($self->ddf($a) eq $self->ddf($b)) {
            $self->_print("%s\n", $self->ddf($a));
        } else {
            $self->_inserted("%s\n", $self->ddf($a));
            $self->_deleted("%s\n", $self->ddf($b));
        }
    }
}

sub _print {
    my ($self, @args) = @_;
    $BUFFER .= ' 'x($LEVEL*$self->{indent});
    $BUFFER .= sprintf colored(['reset'], shift @args), @args;
}

sub _inserted {
    my ($self, @args) = @_;
    $BUFFER .= ' 'x($LEVEL*$self->{indent});
    $BUFFER .= sprintf colored([$self->{"inserted_color"}], shift @args), @args;
}

sub _updated {
    my ($self, @args) = @_;
    $BUFFER .= ' 'x($LEVEL*$self->{indent});
    $BUFFER .= sprintf colored([$self->{"updated_color"}], shift @args), @args;
}

sub _deleted {
    my ($self, @args) = @_;
    $BUFFER .= ' 'x($LEVEL*$self->{indent});
    $BUFFER .= sprintf colored([$self->{"deleted_color"}], shift @args), @args;
}

sub _comment {
    my ($self, @args) = @_;
    $BUFFER .= ' 'x($LEVEL*$self->{indent});
    $BUFFER .= sprintf colored([$self->{"comment_color"}], shift @args), @args;
}

1;
__END__

=encoding utf8

=head1 NAME

Data::Difflet - Ultra special pretty cute diff generator Mark II

=head1 SYNOPSIS

    use Data::Difflet;

    my $difflet = Data::Difflet->new();
    print $difflet->compare(
        {
            a => 2,
            c => 5,
        },
        {
            a => 3,
            b => 4,
        }
    );

=head1 DESCRIPTION

B<THIS MODULE IS IN ITS BETA QUALITY. THE API MAY CHANGE IN THE FUTURE>

Data::Difflet is colorful diff generator for Perl5!

See the following image!

=begin html

<img src="http://gyazo.64p.org/image/a82cb1898b53d51e45e49b21667aec85.png">

=end html

=head1 METHODS

=over 4

=item my $difflet = Data::Difflet->new();

Create new instance of Data::Difflet object.

=item $difflet->compare($a, $b);

Compare the two data and get a colorized strings.

=back

=head1 AUTHOR

Tokuhiro Matsuno E<lt>tokuhirom AAJKLFJEF@ GMAIL COME<gt>

=head1 SEE ALSO

This module is inspired from node.js library named difflet.
L<git://github.com/substack/difflet.git>

=head1 LICENSE

Copyright (C) Tokuhiro Matsuno

This library is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.

=cut
