The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.
package Silki::Schema::PageRevision;
{
  $Silki::Schema::PageRevision::VERSION = '0.29';
}

use strict;
use warnings;
use namespace::autoclean;

use Algorithm::Diff qw( sdiff );
use Encode qw( decode );
use List::AllUtils qw( all any );
use Markdent::CapturedEvents;
use Markdent::Handler::CaptureEvents;
use Markdent::Parser;
use String::Diff qw( diff );
use Silki::Config;
use Silki::Formatter::WikiToHTML;
use Silki::Markdent::Handler::ExtractWikiLinks;
use Silki::Schema;
use Silki::Schema::Page;
use Silki::Schema::PageLink;
use Silki::Schema::PendingPageLink;
use Silki::Types qw( Bool );
use Storable qw( nfreeze thaw );
use Text::TOC::HTML;

use Fey::ORM::Table;
use MooseX::ClassAttribute;
use MooseX::Params::Validate qw( validated_list validated_hash );

with 'Silki::Role::Schema::URIMaker';

with 'Silki::Role::Schema::SystemLogger' => { methods => ['delete'] };

my $Schema = Silki::Schema->Schema();

has_policy 'Silki::Schema::Policy';

has_table( $Schema->table('PageRevision') );

has_one page => (
    table   => $Schema->table('Page'),
    handles => [qw( domain wiki wiki_id )],
);

has_one( $Schema->table('User') );

transform content => deflate {
    return unless defined $_[1];
    $_[1] =~ s/\r\n|\r/\n/g;
    return $_[1];
};

class_has _RenumberHigherRevisionsSQL => (
    is      => 'ro',
    isa     => 'Fey::SQL::Update',
    lazy    => 1,
    builder => '_BuildRenumberHigherRevisionsSQL',
);

with 'Silki::Role::Schema::Serializes';

around insert => sub {
    my $orig  = shift;
    my $class = shift;

    my $revision;

    my @args = @_;

    Silki::Schema->RunInTransaction(
        sub {
            $revision = $class->$orig(@args);
            $revision->_post_change();
        }
    );

    return $revision;
};

around update => sub {
    my $orig = shift;
    my $self = shift;

    my @args = @_;

    Silki::Schema->RunInTransaction(
        sub {
            $self->$orig(@args);
            $self->_post_change();
        }
    );
};

around delete => sub {
    my $orig = shift;
    my $self = shift;

    my %p = @_;

    Silki::Schema->RunInTransaction(
        sub {
            my $rev = $self->revision_number();

            my $page    = $self->page();
            my $max_rev = $page->most_recent_revision()->revision_number();
            my $is_most_recent = $rev == $max_rev;

            $self->$orig(%p);

            $page->_clear_most_recent_revision();

            $self->_renumber_higher_revisions( $rev, $max_rev );

            $page->_clear_revision_count();

            if ( $page->revision_count() ) {
                $page->most_recent_revision()->_post_change()
                    if $is_most_recent;
            }
            else {
                $page->delete( user => $p{user} );
            }
        }
    );
};

sub _renumber_higher_revisions {
    my $self    = shift;
    my $rev     = shift;
    my $max_rev = shift;

    return if $rev == $max_rev;

    my $update = $self->_RenumberHigherRevisionsSQL();

    my $dbh = Silki::Schema->DBIManager()->source_for_sql($update)->dbh();

    for my $r ( $rev + 1 .. $max_rev ) {
        $dbh->do(
            $update->sql($dbh),
            {},
            $self->page_id(),
            $r,
        );
    }

    return;
}

sub _BuildRenumberHigherRevisionsSQL {
    my $class = shift;

    my $update = Silki::Schema->SQLFactoryClass()->new_update();

    my $page_rev_t = $Schema->table('PageRevision');

    my $minus_one = Fey::Literal::Term->new(
        $page_rev_t->column('revision_number'),
        ' - 1'
    );

    #<<<
    $update
        ->update($page_rev_t)
        ->set( $page_rev_t->column('revision_number'),
               $minus_one )
        ->where( $page_rev_t->column('page_id'), '=',
                 Fey::Placeholder->new() )
        ->and  ( $page_rev_t->column('revision_number'), '=',
                 Fey::Placeholder->new() );
    #>>>
    return $update;
}

our $SkipPostChangeHack;

sub _post_change {
    my $self = shift;

    return if $SkipPostChangeHack;

    my ( $existing, $pending, $capture ) = $self->_process_extracted_links();

    my $delete_existing = Silki::Schema->SQLFactoryClass()->new_delete();
    #<<<
    $delete_existing
        ->delete()
        ->from( $Schema->table('PageLink') )
        ->where( $Schema->table('PageLink')->column('from_page_id'),
                 '=', $self->page_id() );
    #>>>

    my $delete_pending = Silki::Schema->SQLFactoryClass()->new_delete();

    #<<<
    $delete_pending
        ->delete()
        ->from( $Schema->table('PendingPageLink') )
        ->where( $Schema->table('PendingPageLink')->column('from_page_id'),
                 '=', $self->page_id() );
    #>>>
    my $update_cached_content
        = Silki::Schema->SQLFactoryClass()->new_update();

    #<<<
    $update_cached_content
        ->update( $Schema->table('Page') )
        ->set( $Schema->table('Page')->column('cached_content') =>
               nfreeze( $capture->captured_events() ) )
        ->where( $Schema->table('Page')->column('page_id'), '=',
                 $self->page_id() );
    #>>>
    my $dbh = Silki::Schema->DBIManager()->source_for_sql($delete_existing)
        ->dbh();

    $dbh->do(
        $delete_existing->sql($dbh),
        {},
        $delete_existing->bind_params()
    );
    $dbh->do(
        $delete_pending->sql($dbh),
        {},
        $delete_pending->bind_params()
    );

    my $sth  = $dbh->prepare( $update_cached_content->sql($dbh) );
    my @bind = $update_cached_content->bind_params();
    $sth->bind_param( 1, $bind[0], { pg_type => DBD::Pg::PG_BYTEA() } );
    $sth->bind_param( 2, $bind[1] );
    $sth->execute();

    Silki::Schema::PageLink->insert_many( @{$existing} )
        if @{$existing};
    Silki::Schema::PendingPageLink->insert_many( @{$pending} )
        if @{$pending};
}

sub _process_extracted_links {
    my $self = shift;

    my $capture = Markdent::Handler::CaptureEvents->new();
    my $linkex  = Silki::Markdent::Handler::ExtractWikiLinks->new(
        page => $self->page(),
        wiki => $self->page()->wiki(),
    );
    my $multi = Markdent::Handler::Multiplexer->new(
        handlers => [ $capture, $linkex ],
    );

    my $filter = Markdent::Handler::HTMLFilter->new( handler => $multi );

    my $parser = Markdent::Parser->new(
        dialect => 'Silki::Markdent::Dialect::Silki',
        handler => $filter,
    );

    $parser->parse( markdown => $self->content() );

    my $links = $linkex->links();

    my @existing = map {
        {
            from_page_id => $self->page_id(),
            to_page_id   => $links->{$_}{page}->page_id(),
        }
        }
        grep { $links->{$_}{page} }
        keys %{$links};

    my @pending = map {
        {
            from_page_id  => $self->page_id(),
            to_wiki_id    => $links->{$_}{wiki}->wiki_id(),
            to_page_title => $links->{$_}{title},
        }
        }
        grep { $links->{$_}{title} && !$links->{$_}{page} }
        keys %{$links};

    return \@existing, \@pending, $capture;
}

sub _system_log_values_for_delete {
    my $self = shift;

    my $page = $self->page();

    my $msg
        = 'Deleted revision '
        . $self->revision_number()
        . ' of the '
        . $page->title()
        . ' page, in wiki '
        . $page->wiki()->title();

    return (
        wiki_id   => $self->wiki_id(),
        page_id   => $self->page_id(),
        message   => $msg,
        data_blob => {
            revision_number   => $self->revision_number(),
            content           => $self->content(),
            user_id           => $self->user_id(),
            creation_datetime => $self->creation_datetime_raw(),
        },
    );
}

sub _base_uri_path {
    my $self = shift;

    my $page = $self->page();

    return $page->_base_uri_path() . '/revision/' . $self->revision_number();
}

sub Diff {
    my $class = shift;
    my ( $rev1, $rev2 ) = validated_list(
        \@_,
        rev1 => { isa => 'Silki::Schema::PageRevision' },
        rev2 => { isa => 'Silki::Schema::PageRevision' },
    );

    my @rev1 = map { s/^\s+|\s+$//; $_ } split /\n\n+/, $rev1->content();
    my @rev2 = map { s/^\s+|\s+$//; $_ } split /\n\n+/, $rev2->content();

    return $class->_BlockLevelDiff( \@rev1, \@rev2 );
}

sub _BlockLevelDiff {
    my $class = shift;
    my $rev1  = shift;
    my $rev2  = shift;

    return $class->_ReorderIfTotalReplacement(
        [
            sdiff(
                $rev1,
                $rev2,
            )
        ]
    );
}

# If the two revisions have nothing in common, we reorder the diff so all the
# inserts come first and the deletes come second. This will show all new
# content first, followed by all the removed old content.
sub _ReorderIfTotalReplacement {
    my $class = shift;
    my $diff  = shift;

    return $diff if any { $_->[0] =~ /[uc]/ } @{$diff};

    return [
        ( grep { $_->[0] eq q{+} } @{$diff} ),
        ( grep { $_->[0] eq q{-} } @{$diff} ),
    ];
}

sub content_as_html {
    my $self = shift;
    my (%p) = validated_hash(
        \@_,
        user        => { isa => 'Silki::Schema::User' },
        include_toc => { isa => Bool, default => 0 },
        for_editor  => { isa => Bool, default => 0 },
    );

    my $page = $self->page();

    my $formatter = Silki::Formatter::WikiToHTML->new(
        %p,
        page => $page,
        wiki => $self->wiki(),
    );

    if ( $self->revision_number()
        == $page->most_recent_revision()->revision_number() ) {

        my $captured = thaw( $page->cached_content() );

        return $formatter->captured_events_to_html($captured);
    }
    else {
        return $formatter->wiki_to_html( $self->content() );
    }
}

__PACKAGE__->meta()->make_immutable();

1;

# ABSTRACT: Represents a page revision

__END__
=pod

=head1 NAME

Silki::Schema::PageRevision - Represents a page revision

=head1 VERSION

version 0.29

=head1 AUTHOR

Dave Rolsky <autarch@urth.org>

=head1 COPYRIGHT AND LICENSE

This software is Copyright (c) 2011 by Dave Rolsky.

This is free software, licensed under:

  The GNU Affero General Public License, Version 3, November 2007

=cut