The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
# BEGIN BPS TAGGED BLOCK {{{
# COPYRIGHT:
# 
# This software is Copyright (c) 2003-2008 Best Practical Solutions, LLC
#                                          <clkao@bestpractical.com>
# 
# (Except where explicitly superseded by other copyright notices)
# 
# 
# LICENSE:
# 
# 
# This program is free software; you can redistribute it and/or
# modify it under the terms of either:
# 
#   a) Version 2 of the GNU General Public License.  You should have
#      received a copy of the GNU General Public License along with this
#      program.  If not, write to the Free Software Foundation, Inc., 51
#      Franklin Street, Fifth Floor, Boston, MA 02110-1301 or visit
#      their web page on the internet at
#      http://www.gnu.org/copyleft/gpl.html.
# 
#   b) Version 1 of Perl's "Artistic License".  You should have received
#      a copy of the Artistic License with this package, in the file
#      named "ARTISTIC".  The license is also available at
#      http://opensource.org/licenses/artistic-license.php.
# 
# This work is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
# 
# CONTRIBUTION SUBMISSION POLICY:
# 
# (The following paragraph is not intended to limit the rights granted
# to you to modify and distribute this software under the terms of the
# GNU General Public License and is only of importance to you if you
# choose to contribute your changes and enhancements to the community
# by submitting them to Best Practical Solutions, LLC.)
# 
# By intentionally submitting any modifications, corrections or
# derivatives to this work, or any other work intended for use with SVK,
# to Best Practical Solutions, LLC, you confirm that you are the
# copyright holder for those contributions and you grant Best Practical
# Solutions, LLC a nonexclusive, worldwide, irrevocable, royalty-free,
# perpetual, license to use, copy, create derivative works based on
# those contributions, and sublicense and distribute those contributions
# and any derivatives thereof.
# 
# END BPS TAGGED BLOCK }}}
package SVK::Editor::InteractiveStatus;

use strict;
use warnings;

use Algorithm::Diff;
use SVK::I18N;
use SVN::Delta;

use SVK::Version;  our $VERSION = $SVK::VERSION;
use SVK::Editor::Patch;
use Algorithm::Diff;

#our @ISA = qw(SVN::Delta::Editor);

sub new {
    my ($class, @args) = @_;
    #my $self = $class->SUPER::new (@arg);
    my $self = bless {@args}, ref $class || $class;

    $self->{files} = {};
    $self->{conflicts} = [];

    return $self;
}

sub close_edit {
    my ($self, $pool) = @_;

    if (@{$self->{conflicts}}) {
        my $msg = "  ".join("\n  ", @{$self->{conflicts}});

        if (@{$self->{conflicts}} == 1) {
            $msg = loc("Conflict detected in:\n%1\n".
                "file. Do you want to skip it and commit other changes? (y/n) ",
                $msg);
        } else {
            $msg = loc("Conflict detected in:\n%1\n".
                "files. Do you want to skip those and commit other changes? (y/n) ",
                $msg);
        }

        if (SVK::Util::get_prompt($msg, qr/^[yn]$/i) =~ /[Nn]/) {
            $_->on_end_selection_phase($self)
                for @{$self->{actions}};

            for (@{$self->{conflicts}}) {
                $self->{notify}->node_status($_, 'C');
                $self->{notify}->flush($_);
            }
                
            $_->on_end_selection_phase_last_chance($self)
                for @{$self->{actions}};
            return;
        } else {
            my (%actions, @actions);
            @actions = map {delete $self->{info}{$_}} @{$self->{conflicts}};
            # flatten actions list.
            push @actions, @{$_->{children}} for @actions;
            @actions = map {$_->{id}} @actions;

            @actions{@actions} = 1;

            $self->{actions} =
                [grep {not exists $actions{$_->{id}}} @{$self->{actions}}];
        }
    }

    my $ui = SVK::Editor::InteractiveStatus::UI->new($self);
    $ui->run;
    # Should this be on close edit???
    $_->on_end_selection_phase($self)
        for @{$self->{actions}};
    $_->on_end_selection_phase_last_chance($self)
        for @{$self->{actions}};
}

sub abort_edit {
    my ($self, $pool) = @_;

}

sub open_root {
    my ($self, $baserev, $pool) = @_;

    push @{$self->{actions}}, $self->{info}{''} =
        SVK::Editor::InteractiveStatus::OpenDirectoryAction->
            new(undef, $self, '', '', $baserev, $pool);

    return '';
}

sub add_file {
    my ($self, $path, $pdir, $copy_path, $rev, $pool) = @_;

    push @{$self->{actions}}, $self->{info}{$path} =
        SVK::Editor::InteractiveStatus::AddFileAction->new($pdir, @_);
    
    return $path;
}

sub open_file {
    my ($self, $path, $pdir, $rev, $pool) = @_;

    push @{$self->{actions}}, $self->{info}{$path} =
        SVK::Editor::InteractiveStatus::ModifyFileAction->new($pdir, @_);

    return $path;
}

sub apply_textdelta {
    my ($self, $path, $checksum, $pool) = @_;
    my $action = $self->{info}{$path};

    return $action->on_apply_textdelta(@_);
}

sub change_file_prop {
    my ($self, $path, $name, $value, $pool) = @_;
    
    push @{$self->{actions}}, $self->{info}{$path}{props}{$name} = 
        SVK::Editor::InteractiveStatus::ModifyFilePropAction->new($path, @_);
}

sub close_file {
    my ($self, $path, $checksum, $pool) = @_;
    my $action = $self->{info}{$path};

    $action->on_close_file(@_);
}

sub delete_entry {
    my ($self, $path, $rev, $pdir, $pool) = @_;

    my $parent = $path;

    do {
        $parent =~ s{/[^/]*$}{};
    } while (not $self->{info}{$parent});

    return if $pdir ne $parent;

    push @{$self->{actions}}, $self->{info}{$path} =
        SVK::Editor::InteractiveStatus::DeleteFileAction->new($parent, @_);
}

sub add_directory {
    my ($self, $path, $pdir, $copy_from, $rev, $pool) = @_;

    push @{$self->{actions}}, $self->{info}{$path} =
        SVK::Editor::InteractiveStatus::AddDirectoryAction->new($pdir, @_);

    return $path;
}

sub open_directory {
    my ($self, $path, $pdir, $rev, $pool) = @_;

    push @{$self->{actions}}, $self->{info}{$path} =
        SVK::Editor::InteractiveStatus::OpenDirectoryAction->new($pdir, @_);

    return $path;
}

sub change_dir_prop {
    my ($self, $path, $name, $value, $pool) = @_;

    push @{$self->{actions}}, $self->{info}{$path}{props}{$name} = 
        SVK::Editor::InteractiveStatus::ModifyDirectoryPropAction->new($path, @_);
}

sub close_directory {
    my ($self, $path) = @_;
    my $action = $self->{info}{$path};

    $action->on_close_directory(@_);
}

sub conflict {
    my ($self, $path) = @_;

    push @{$self->{conflicts}}, $path;
}

package SVK::Editor::InteractiveStatus::UI;
use SVK::I18N;

sub new {
    my ($class, $editor) = @_;
    my $qcount = 0;

    my $self = bless {
        actions => $editor->{actions},
        current_question => -1,
        current_action => 0,
        current_action_question => -1
    }, $class;

    $qcount += $_->get_questions_count for @{$self->{actions}};
    $self->{questions_count} = $qcount;

    return $self;
}

sub _loc {
    my ($text, $keys, @keys) = @_;

    ($text, @keys) = split /#/, $text;
    return ($text, join("",@keys), $keys);
}

my %prompts = (
    basic =>
        [_loc("[a]ccept, [s]kip this change#a#s", "as")],
    fileChunks =>
        [_loc("[A]ccept, [S]kip the rest of changes to this file#A#S", "AS")],
    fileProps =>
        [_loc("a[c]cept, s[k]ip rest of changes to this file and its properties#c#k", "ck")],
    dirSubdir =>
        [_loc("[A]ccept, [S]kip changes to whole subdirectory#A#S", "AS")],
    dirSubdirAcceptOnly =>
        [_loc("[A]ccept changes to whole subdirectory#A", "A")],
    propsFile =>
        [_loc("[A]ccept, [S]kip the rest of changes to this file properties#A#S", "AS")],
    propsDir =>
        [_loc("[A]ccept, [S]kip the rest of changes to this directory properties#A#S", "AS")],
    props =>
        [_loc("a[c]cept, s[k]ip changes to all properties with that name#c#k", "ck")],
    move_back =>
        [_loc("move to [p]revious change#p", "p")],
);

sub run {
    my $self = shift;
    my ($question, $flags, $state, $change_info, $action, $regexp);

    $self->next_question;

    while ($self->{current_question} < $self->{questions_count}) {
        my ($answers, $keys, @prompts);

        $action = $self->{actions}[$self->{current_action}];

        ($question, $flags, $state, $change_info) =
            $action->get_question($self->{current_action_question});
        
        my @flags = ('basic', @$flags);
        push @flags, 'move_back' if $self->{current_question} > 0;

        for (@flags) {
            push @prompts, $prompts{$_}->[0];
            $keys.= $prompts{$_}->[1];
            $answers.= $prompts{$_}->[2];
        }

        $question = ($change_info||"")."\n[".
            ($self->{current_question}+1)."/$self->{questions_count}] ".
            "$question:\n".join(",\n", @prompts);

        if ($$state) {
            $regexp = qr/^[$keys]?$/;
            $question.= " [$$state]> ";
        } else {
            $regexp = qr/^[$keys]$/;
            $question.= " > ";
        }

        my $answer = SVK::Util::get_prompt($question, $regexp) || $$state;
        $answer =~ eval "\$answer =~ tr/$keys/$answers/";

        if ($answer eq 'p') {
            $self->previous_question;
            next;
        }

        my $res = $action->on_state_update(
            $self->{current_action_question}, $answer);

        $$state = $answer;
        $self->update_questions_count if $res;

        $self->next_question;
    }
}

sub update_questions_count {
    my $self = shift;
    my $qcount = 0;

    $qcount += $_->get_questions_count
        for @{$self->{actions}}[0..$self->{current_action}-1];

    $self->{current_question} = $self->{current_action_question} + $qcount;

    $qcount += $_->get_questions_count
        for @{$self->{actions}}[$self->{current_action}..$#{$self->{actions}}];

    $self->{questions_count} = $qcount;
}

sub next_question {
    my $self = shift;
    my $qid = $self->{current_action_question}+1;
    my $aid = $self->{current_action};

    $self->{current_question}++;

    do {
        if ($self->{actions}[$aid]->get_questions_count > $qid) {
            $self->{current_action_question} = $qid;
            $self->{current_action} = $aid;

            return 1;
        }
        $qid = 0;
        $aid++;
    } while ($aid < @{$self->{actions}});

    return 0;
}

sub previous_question {
    my $self = shift;

    return 0 if $self->{current_question} == 0;

    if ($self->{current_action_question} > 0) {
        --$self->{current_action_question};
        --$self->{current_question};
        return 1;
    }

    my $aid = $self->{current_action}-1;
    $aid-- while $self->{actions}[$aid]->get_questions_count <= 0;

    $self->{current_action_question} =
        $self->{actions}[$aid]->get_questions_count - 1;
    $self->{current_action} = $aid;
    $self->{current_question}--;

    return 1;
}

package SVK::Editor::InteractiveStatus::Action;

sub AUTOLOAD {
    no strict 'refs';
    our $AUTOLOAD;
    my ($self, @arg) = @_;
    my $name = $AUTOLOAD;
    $name =~ s/^.*::on_(.*)_commit$/$1/;

    return unless $name and grep {$name eq $_} qw(delete_entry
        add_file open_file add_directory open_directory apply_textdelta
        change_file_prop close_file change_dir_prop close_directory);

    my $pos = SVK::Editor->baton_at($name);
    if ($pos >= 0) {
        *$AUTOLOAD = sub {
            my ($action, $editor, @args) = @_;
            $args[$pos] = $editor->{storage_baton}{$args[$pos]};
            $editor->{storage}->$name(@args);
        }
    } else {
        *$AUTOLOAD = sub {
            my ($action, $editor, @args) = @_;
            $editor->{storage}->$name(@args);
        }
    }

    goto &$AUTOLOAD;
}

my %globalPropsState;
my $currId = 0;

sub new {
    my ($class, $parent, $editor, $path) = @_;

    my $self = bless {
        path => $path,
        id => $currId++,
        state => '',
        force_disable => {},
        force_enable => {},
        children => [],
    }, $class;

    $parent = $editor->{info}{$parent} if defined $parent;
    push @{$parent->{children}}, $self if $parent;

    return $self;
}

sub on_apply_textdelta { }
sub on_close_file { }
sub on_close_directory { }

sub enabled {
    my $self = shift;

    return 0 if %{$self->{force_disable}};
    return 1 if %{$self->{force_enable}};

    return $self->{enabled} ? 1 : 0;
}

sub get_questions_count {
    my $self = shift;

    return %{$self->{force_disable}} ||
           %{$self->{force_enable}} ? 0 : 1;
}

sub update_children_state {
    my ($self, $state, $by_who) = @_;

    return grep {$_->on_state_update(undef, $state, $by_who || $self)}
        @{$self->{children}};
}

sub on_state_update {
    my ($self, $id, $state, $by_who) = @_;

    if ($by_who) {
        if ($state =~ /[Ac]/) {
            return 0 if exists $self->{force_enable}{$by_who->{id}};

            delete $self->{force_disable}->{$by_who->{id}};
            $self->{force_enable}{$by_who->{id}} = 1;
        } elsif ($state =~ /[Sk]/) {
            return 0 if exists $self->{force_disable}{$by_who->{id}};

            delete $self->{force_enable}->{$by_who->{id}};
            $self->{force_disable}{$by_who->{id}} = 1;
        } else {
            my $ret = delete $self->{force_enable}->{$by_who->{id}};
            $ret ||= delete $self->{force_disable}->{$by_who->{id}};
            return 0 if not $ret;

            $self->update_children_state($state, $by_who);
            return 1;
        }
        $self->update_children_state($state, $by_who);

        return 1;
    }
    $self->{enabled} = $state =~ /[aAc]/;

    return 0;
}

sub on_end_selection_phase {
    my ($self, $editor) = @_;
    $editor->{notify}->node_status($self->{path}, '');
}

sub on_end_selection_phase_last_chance {
    my ($self, $editor) = @_;

    $editor->{notify}->flush($self->{path});
}

package SVK::Editor::InteractiveStatus::AddFileAction;
use base qw(SVK::Editor::InteractiveStatus::Action);

use SVK::I18N;

sub on_apply_textdelta_commit {
    my ($self, $editor, $path, $checksum, $pool) = @_;

    return $self->SUPER::on_apply_textdelta_commit(@_[1..$#_])
        if $self->enabled;

    return undef;
}

sub get_question {
    my ($self, $id) = @_;

    return (loc("File '%1' is marked for addition", $self->{path}),
        [], \$self->{state});
}

sub on_state_update {
    my ($self, $id, $state, $by_who) = @_;

    my $res = $self->SUPER::on_state_update($id, $state, $by_who);
    return $res if $by_who;

    return 0 if $state eq $self->{state};

    return $self->update_children_state($state eq 's' ? 'S' : $state)
        if $self->{state} =~ /[s]/ or $state =~ /[s]/;

    return 0;
}

sub on_end_selection_phase {
    my ($self, $editor) = @_;

    if ($self->enabled) {
        $editor->{notify}->node_status($self->{path}, 'A');
    } else {
        $editor->{notify}->node_status($self->{path}, '');
        $editor->{cb_skip_add}($self->{path});
    }
}

package SVK::Editor::InteractiveStatus::ModifyFileAction;
use base qw(SVK::Editor::InteractiveStatus::Action);

use List::Util qw(min max);
use Digest::MD5 qw(md5_hex);
use SVK::Util qw(mimetype_is_text $EOL);
use SVK::I18N;

sub new {
    my ($class, $parent, $editor, $path, $pdir, $rev, $pool) = @_;
    my $self = $class->SUPER::new(@_[1..$#_]);

    $self->{rev} = $rev;

    return $self
}

sub on_apply_textdelta {
    my ($self, $editor, $path, $checksum, $pool) = @_;

    my ($type) = grep {$_->{name} eq 'svn:mime-type'}
        @{$self->{children}};

    if ($type and !mimetype_is_text($type->{value}) or
        ($type = $editor->{inspector}->localprop($path, 'svn:mime-type', $pool)) and
        !mimetype_is_text($type))
    {
        bless $self,
            "SVK::Editor::InteractiveStatus::ModifyBinaryFileAction";
        return $self->on_apply_textdelta(@_[1..$#_]);
    }
    
    my $fh1 = $editor->{inspector}->localmod($path, '', $pool)->[0];

    {
        # XXX: some swig build doesn't like like mg $/ used in
        # SVN::Stream::readline, so we have to deal with the complicated last newline
        local $/;
        my $buf = <$fh1>;
        if (length $buf) {
            $self->{old_content} = [map { "$_\n" } $buf =~ m/^.*$/mg ];
            substr($self->{old_content}[-1], -1, 1, '')
                if substr($buf, -1, 1) ne "\n";
        }
    }
    $self->{new_content} = '';
    open my $fh2, '>', \$self->{new_content};

    $self->{original_checksum} = $checksum;

    return [SVN::TxDelta::apply($fh1, $fh2,
                                undef, undef, $pool)];
}

sub on_apply_textdelta_commit {
    my ($self, $editor, $path, $checksum, $pool) = @_;
    return undef if $self->{empty_change};

    my $handle = SVK::Editor::InteractiveStatus::Action::on_apply_textdelta_commit(@_);

    return $handle if $self->{full_change};
    
    if ($handle && $#{$handle} >= 0) {
        open my $nfh, '<', \$self->{new_content};
        if ($editor->{send_fulltext}) {
            SVN::TxDelta::send_stream($nfh, @$handle, $self->{pool});
        } else {
            my $ofh = $editor->{inspector}->localmod($path, '', $pool)->[0];
            my $txstream = SVN::TxDelta::new($ofh, $nfh, $pool);

            SVN::TxDelta::send_txstream($txstream, @$handle, $self->{pool});
        }
    }
    return undef;
}

sub on_close_file_commit {
    my ($self, $editor, $path, $checksum, $pool) = @_;

    $editor->{storage}->close_file($editor->{storage_baton}{$path},
        $self->{checksum}, $pool);
}

sub enabled {
    my $self = shift;

    return 0 if %{$self->{force_disable}};
    return 1 if %{$self->{force_enable}};

    return 1 if grep {$_ =~ /[aAc]/} @{$self->{states}};

    return grep {$_->enabled} @{$self->{children}};
}

sub get_questions_count {
    my $self = shift;

    unless (exists $self->{chunks}) {
        $self->split_diff_into_chunks;
    }

    return $self->{cutoff};
}

sub get_question {
    my ($self, $id) = @_;
    my @flags;

    push @flags, "fileChunks" if $id*2+1 < $#{$self->{chunks}};
    push @flags, "fileProps" if @{$self->{children}};

    return (loc("Modification to '%1' file", $self->{path}),
        \@flags, \$self->{states}[$id], $self->{chunks}[$id*2+1][2]);
}

sub on_state_update {
    my ($self, $id, $state, $by_who) = @_;

    my $res = $self->SUPER::on_state_update($id, $state, $by_who);
    return $res if $by_who;

    return 0 if $state eq $self->{states}[$id];

    if ($self->{states}[$id] =~ /[ck]/ or $state =~ /[ck]/) {
        $res = $self->update_children_state($state =~ /[AS]/ ? 'a' : $state);
    }

    if ($state =~ /[ASck]/) {
        ++$id;
        return $self->{cutoff} = $id if $id != $self->{cutoff};
        return $res;
    }

    $id = $self->{cutoff};
    $self->{cutoff} = int(@{$self->{chunks}}/2);

    return $id ne $self->{cutoff};
}

use constant CONTEXT => 5;

sub split_diff_into_chunks {
    my $self = shift;
    my ($p, $b1, $b2, $d1, $d2, $i1, $i2, $a1, $a2) = (0);
    my $di;

    my @old_content = $self->{old_content} ? @{$self->{old_content}} : ();
    delete $self->{old_content};

    my @new_content = defined $self->{new_content} ?
        $self->{new_content} =~ m/.*\n?/g : (1);
    pop @new_content;

    my ($lln, $rln) = (
        !(@old_content and $old_content[-1] =~ /\n$/),
        !(@new_content and $new_content[-1] =~ /\n$/));

    my ($lo, $ln) = (@old_content-1, @new_content-1);
    my $diff = new Algorithm::Diff(\@old_content, \@new_content);

    while ($diff->Next()) {
        next if $diff->Same();

        ($d1, $d2, $i1, $i2) = $diff->Get(qw(Min1 Max1 Min2 Max2));

        if (not $diff->Items(2)) {
            ($b1, $b2) = (max(0, $d1 - CONTEXT), max(-1, $d1 - 1));
            ($a1, $a2) = (min($lo + 1, $d2 + 1), min($lo, $d2 + CONTEXT));
        } elsif (not $diff->Items(1)) {
            ($b1, $b2) = (max(0, $d2 - CONTEXT - 1), $d2);
            ($a1, $a2) = (min($lo + 1, $b2 + 1), min($lo, $b2 + CONTEXT));
        } else {
            ($b1, $b2) = (max(0, $d1 - CONTEXT), max(-1, $d1 - 1));
            ($a1, $a2) = (min($lo + 1, $d2 + 1), min($lo, $d2 + CONTEXT));
        }
        $di = "--- $self->{path}\t(revision $self->{rev})".$EOL;
        $di.= "+++ $self->{path}\t(local)".$EOL;
        my ($l1, $l2) = map {$_ < 1 ? "" : ",$_" } $a2-$b1, $i2-$i1+$a2-$b1;
        $di.= "@@ -$b1$l1 +@{[$i1+$b1-$d1]}$l2 @@".$EOL;
        $di.= join " ","",@old_content[$b1..$b2] if $b1 <= $b2;
        if ($d1 <= $d2) {
            $di.= join "-","",@old_content[$d1..$d2];
            $di.= "\n\\ No newline at end of file".$EOL if $lln and $d2 == $lo;
        }
        if ($i1 <= $i2) {
            $di.= join "+","",@new_content[$i1..$i2];
            $di.= "\n\\ No newline at end of file".$EOL if $rln and $i2 == $ln;
        }
        $di.= join " ","",@old_content[$a1..$a2] if $a1 <= $a2;

        my $b = $p <= $b2  ? join "",@old_content[$p..$b2]  : "";
        my $d = $d1 <= $d2 ? join "",@old_content[$d1..$d2] : "";
        my $i = $i1 <= $i2 ? join "",@new_content[$i1..$i2] : "";

        $p = $a1;

        push @{$self->{chunks}}, $b, [$d, $i, $di];
    }
    push(@{$self->{chunks}}, join("",@old_content[$p..$lo]));

    $self->{cutoff} = int(@{$self->{chunks}}/2);
    $self->{states} = [('') x $self->{cutoff}];
}

sub on_end_selection_phase {
    my ($self, $editor) = @_;

    unless (exists $self->{chunks}) {
        $self->split_diff_into_chunks;
    }

    my @states = map {$_ =~ /[aAc]/ ? 1 : 0} @{$self->{states}};

    @states[$self->{cutoff}..@states] =
        ($self->{cutoff} ? $states[$self->{cutoff}-1] : '') x (@states - $self->{cutoff});

    unless (grep {$_} @states) {
        $editor->{notify}->node_status($self->{path}, '');
        return $self->{empty_change} = 1;
    }

    $editor->{notify}->node_status($self->{path}, 'M');

    return $self->{full_change} = 1 unless grep {!$_} @states;

    my $v = '';
    for (my $idx = 1; $idx < @{$self->{chunks}}; $idx += 2) {
        $v.= $self->{chunks}[$idx-1];
        $v.= $self->{chunks}[$idx][$states[$idx/2]];
    }
    $self->{new_content} = $v.$self->{chunks}[-1];
    $self->{checksum} = md5_hex($self->{new_content});
}

package SVK::Editor::InteractiveStatus::ModifyBinaryFileAction;
use base qw(SVK::Editor::InteractiveStatus::AddFileAction);

use SVK::I18N;

sub get_question {
    my ($self, $id) = @_;
    my @flags;

    push @flags, "fileProps" if @{$self->{children}};

    return (loc("Modifications to binary file '%1'", $self->{path}),
        \@flags, \$self->{state});
}

sub on_state_update {
    my ($self, $id, $state, $by_who) = @_;

    my $res = $self->SUPER::on_state_update($id, $state, $by_who);
    return $res if $by_who;

    if ($state =~ /[ck]/ or $self->{state} =~ /[ck]/) {
        return $self->update_children_state($state);
    }

    return 0;
}

sub on_end_selection_phase {
    my ($self, $editor) = @_;

    $editor->{notify}->node_status($self->{path}, $self->enabled ? 'U' : '');
}

package SVK::Editor::InteractiveStatus::DeleteFileAction;
use base qw(SVK::Editor::InteractiveStatus::Action);

use SVK::I18N;

sub get_question {
    my ($self, $id) = @_;

    return (loc("File or directory '%1' is marked for deletion", $self->{path}),
        [], \$self->{state});
}

sub on_end_selection_phase {
    my ($self, $editor) = @_;

    $editor->{notify}->node_status($self->{path}, 'D') if $self->{enabled}
}

package SVK::Editor::InteractiveStatus::ModifyFilePropAction;
use base qw(SVK::Editor::InteractiveStatus::Action);

use SVK::I18N;
use SVK::Util qw(tmpfile slurp_fh);

sub new {
    my ($class, $parent, $editor, $path, $name, $value, $pool) = @_;
    my $self = $class->SUPER::new(@_[1..$#_]);

    my ($l, $r) = $editor->{inspector}->localprop($path, $name, $pool);
    ($l, $r) = map { !length || /\n$/ ? $_ : "$_\n"}
        defined $l ? $l : "", defined $value ? $value : "";

    my ($lfh, $lfn) = tmpfile('diff'); 
    my ($rfh, $rfn) = tmpfile('diff'); 

    slurp_fh($l, $lfh); close($lfh);
    slurp_fh($r, $rfh); close($rfh);

    my $diff = '';
    open my $fh, '>', \$diff;

    my $dh = SVN::Core::diff_file_diff($lfn, $rfn);
    SVN::Core::diff_file_output_unified($fh, $dh, $lfn, $rfn,
        '', '', $pool);

    $diff =~ s/.*\n.*\n//;
    $diff =~ s/^\@.*\n//mg;
    $diff =~ s/^/ /mg;

    $self->{name} = $name;
    $self->{value} = $value;
    $self->{diff} =
        "Property change on $path\n".("_" x 67)."\n".
        "Name: $name\n$diff";

    return $self;
}

sub get_questions_count {
    my $self = shift;

    return %{$self->{force_disable}} ||
           %{$self->{force_enable}} ||
           exists $globalPropsState{$self->{name}} &&
                $self->{state} !~ /[ck]/ ? 0 : 1;
}

sub get_question {
    my ($self, $id) = @_;
    my @flags = qw(props);

#    unshift @flags, "propsFile" if $id*2 < @{$self->{chunks}};

    return (loc("Property change on '%1' file requested", $self->{path}),
        \@flags, \$self->{state}, $self->{diff});
}

sub on_state_update {
    my ($self, $id, $state, $by_who) = @_;

    my $res = $self->SUPER::on_state_update($id, $state, $by_who);
    return $res if $by_who;

    if ($state ne $self->{state}) {
        if ($state eq 'c') {
            $globalPropsState{$self->{name}} = 1;
        } elsif ($state eq 'k') {
            $globalPropsState{$self->{name}} = 0;
        } elsif ($self->{state} =~ /[ck]/) {
            delete $globalPropsState{$self->{name}};
        } else {
            return 0;
        }
        return 1;
    }
    return 0;
}

sub enabled {
    my $self = shift;
    my $globPropState = $globalPropsState{$self->{name}};

    return 0 if %{$self->{force_disable}} or
        defined $globPropState and not $globPropState;
    return 1 if %{$self->{force_enable}} or
        $globPropState;

    return $self->{enabled};
}

sub on_end_selection_phase {
    my ($self, $editor) = @_;

    if ($self->enabled) {
        $editor->{notify}->prop_status($self->{path}, 'M');
    } else {
        $editor->{cb_skip_prop_change}($self->{path},
            $self->{name}, $self->{value});
    }
}

sub on_end_selection_phase_last_chance { }

package SVK::Editor::InteractiveStatus::AddDirectoryAction;
use base qw(SVK::Editor::InteractiveStatus::Action);

use SVK::I18N;

sub get_question {
    my ($self, $id) = @_;
    my @flags = ();

    push @flags, "dirSubdirAcceptOnly"
        if grep { (ref $_) =~ '::Add(?:File|Directory)Action$' } @{$self->{children}};

    return (loc("Directory '%1' is marked for addition", $self->{path}),
        \@flags, \$self->{state});
}

sub on_state_update {
    my ($self, $id, $state, $by_who) = @_;

    my $res = $self->SUPER::on_state_update($id, $state, $by_who);
    return $res if $by_who;

    return $self->update_children_state($state eq 's' ? 'S' : $state)
        if $state ne $self->{state} and ($self->{state} or $state =~ /[sA]/);

    return 0;
}

sub on_end_selection_phase {
    my ($self, $editor) = @_;

    if ($self->enabled) {
        $editor->{notify}->node_status($self->{path}, 'A');
    } else {
        $editor->{notify}->node_status($self->{path}, '');
        $editor->{cb_skip_add}($self->{path});
    }
}

sub on_end_selection_phase_last_chance {
    my ($self, $editor) = @_;

    $editor->{notify}->flush($self->{path});
}

package SVK::Editor::InteractiveStatus::OpenDirectoryAction;
use base qw(SVK::Editor::InteractiveStatus::Action);

sub get_questions_count {
    return 0;
}

package SVK::Editor::InteractiveStatus::ModifyDirectoryPropAction;
use base qw(SVK::Editor::InteractiveStatus::ModifyFilePropAction);

use SVK::I18N;

sub get_question {
    my ($self, $id) = @_;
    my @flags = qw(props);
    my $path = $self->{path} eq '' ? "." : "$self->{path}";

#    unshift @flags, "propsFile" if $id*2 < @{$self->{chunks}};

    return (loc("Property change on '%1' directory requested", $path),
        \@flags, \$self->{state}, $self->{diff});
}

1;