#!/usr/bin/perl
use strict;
#################### main pod documentation begin ###################

=head1 NAME

 Zymonic - Script to update label ids. Needed for when DisplayName or
 Short Descriptions change, the label digest/id changes.

=head1 SYNOPSIS

=head1 DESCRIPTION

This script will take a label file, repo location and before/after revisions.
Will then generate a new label file updating any labels ids which may have
changed between the revision,

=head1 SUPPORT

As in the license, Zymonic is provided without warranty or support
unless purchased separately, however... If you email
zymonic-support@zednax.com your issue will   be noted and may
receive a response.

For security issues, please contact zymonic-security@zednax.com and
someone will respond within 8 working hours.


=head1 AUTHOR

    Alex Masidlover et al.
    CPAN ID: MODAUTHOR
    Zednax Limited
    alex.masidlover@zednax.com
    http://www.zednax.com

=head1 COPYRIGHT

This program is free software licensed under the...

    Zymonic Public License 1.0

The full text of the license can be found in the
LICENSE file included with this module.

Other licenses may be acceptable if including
parts of Zymonic in larger projects, please
contact Zednax for details. 

=head1 SEE ALSO

perl(1).

=cut

#################### main pod documentation end ###################

# modules
use Data::Dumper;
use Digest::MD5 qw(md5_hex);
use Digest::SHA qw(sha256_hex);
use Encode qw(encode_utf8);
use File::Slurp;
use Text::Diff::Parser;

# temporary directory
my $tmp_dir = '/tmp/label_updates';

# setup script
use Zymonic::Script;
my $s = Zymonic::Script->new(
    args => {
        system =>
          { type => 'string', description => 'System to update label ids in, either this or label_file is required', },
        label_file =>
          { type => 'string', description => 'Label file to update, either this or label_file is required', },
        output      => { type => 'string', description => 'File to output to, required if setting label_file', },
        zymonic_dir => {
            type        => 'string',
            required    => 'true',
            description => 'Location of Zymonic repo, for doing diffs',
        },
        zymonic_r1 => {
            type        => 'int',
            required    => 'true',
            description => 'Revision of the Zymonic repo where labels are currently used',
        },
        zymonic_r2 => {
            type        => 'int',
            required    => 'true',
            description => 'Revision of the Zymonic repo where labels need to be updated for',
        },
        zcps_dir => {
            type        => 'string',
            required    => 'true',
            description => 'Location of zcps repo, for doing diffs',
        },
        zcps_r1 => {
            type        => 'int',
            required    => 'true',
            description => 'Revision of the zcps repo where labels are currently used',
        },
        zcps_r2 => {
            type        => 'int',
            required    => 'true',
            description => 'Revision of the zcps repo where labels need to be updated for',
        },
    },

    # add confirmation step since it'll be updating the DB
    confirmation_condition => ['system'],
    confirmation =>
      "This change will update label ids within the system DB. Previous verison label ids will be removed.\n"
      . "Please ensure you have backed up the system DB before running this if it is a live environment.\n"
      . "Continue? (Y/N) ",
    confirmation_regex => qr/y/i,
);

main();
exit(0);

#################### subroutine header begin ####################

=head2 main 

 Usage     : main();
 Purpose   : main script functionality
 Returns   : nothing
 Argument  : nothing
 Throws    : nothing
 Comment   : 


See Also   :

=cut

#################### subroutine header end ###################

sub main
{
    # if we have a system param we are updating the zymonic system
    my $system = $s->get_arg('system');

    # if we have a label file, read its contents
    my $label_file    = $s->get_arg('label_file');
    my $output_file   = $s->get_arg('output');
    my $file_contents = read_file($label_file) if $label_file;

    # need one or the other
    unless ( $system || ( $label_file && $output_file ) )
    {
        $s->usage( ['system or label_file with output'] );
    }

    # zymonic diffs
    mkdir $tmp_dir unless -d $tmp_dir;
    my $zymonic_dir      = $s->get_arg('zymonic_dir');
    my $zymonic_r_before = $s->get_arg('zymonic_r1');
    my $zymonic_r_after  = $s->get_arg('zymonic_r2');
    my $zymonic_diff     = "$tmp_dir/zymonic.diff";

    # no easy way to excluse all but xml files, originally was using filterdiff, but not available on all platforms
    my $zymonic_diff_cmd =
        "for f in `find $zymonic_dir -iname '*.xml'`; "
      . "do svn diff -r$zymonic_r_before:$zymonic_r_after \$f >> $zymonic_diff 2> $tmp_dir/zymonic.diff.err; done";
    $s->log("Getting zymonic label changes -r$zymonic_r_before:$zymonic_r_after");
    system($zymonic_diff_cmd);
    my @zymonic_label_changes = get_changed_labels($zymonic_diff);

    # zcps diffs
    my $zcps_dir      = $s->get_arg('zcps_dir');
    my $zcps_r_before = $s->get_arg('zcps_r1');
    my $zcps_r_after  = $s->get_arg('zcps_r2');
    my $zcps_diff     = "$tmp_dir/zcps.diff";
    system("touch $zcps_diff");

    # no easy way to excluse all but xml files, originally was using filterdiff, but not available on all platforms
    my $zcps_diff_cmd =
        "for f in `find $zcps_dir -iname '*.xml'`;  "
      . "do svn diff -r$zcps_r_before:$zcps_r_after \$f >> $zcps_diff 2> $tmp_dir/zcps.diff.err; done";
    $s->log("Getting zcps label changes -r$zcps_r_before:$zcps_r_after");
    system($zcps_diff_cmd);
    my @zcps_label_changes = get_changed_labels($zcps_diff);

    # combine changes
    my @label_changes = ( @zymonic_label_changes, @zcps_label_changes );

    # if we are updating label file, do that now
    if ($label_file)
    {
        my $new_file_contents = update_label_ids( $file_contents, \@label_changes );

        # write out the file
        write_file( $output_file, $new_file_contents ) or die $!;
        $s->log("New Label File generated: $output_file");
    }

    # if we are updating system do that now
    if ($system)
    {
        $s->setup_zymonic_objects();
        update_system_labels( $system, \@label_changes );
        $s->log("Updated label ids in $system DB");
    }

    # cleanup
    unlink glob "$tmp_dir/*";
    rmdir $tmp_dir;
}

#################### subroutine header begin ####################

=head2 get_changed_labels 

 Usage     : get_changed_labels( $diff_file);
 Purpose   : parses the diffs and returns changed labels
 Returns   : nothing
 Argument  : text file
 Throws    : nothing
 Comment   : 


See Also   :

=cut

#################### subroutine header end ###################

sub get_changed_labels
{
    my $diff_file = shift;

    # parse the diffs
    my $p = Text::Diff::Parser->new($diff_file);
    $p->simplify();

    # iterate over changes
    my @label_changes = ();
    my %current_change;
    foreach my $change ( $p->changes )
    {
        next unless $change->filename1() =~ /\.xml/;
        my $change_text = join( "\n", $change->text() );
        if ( $change_text =~ /DisplayName|ShortDescription/ )
        {
            my $type = $change->type();
            if ( $type eq 'REMOVE' )
            {
                %current_change = ();
                my $display_name = extract_label( 'DisplayName', $change_text );
                if ($display_name)
                {
                    $current_change{dn1} = $display_name;
                }

                my $description = extract_label( 'ShortDescription', $change_text );
                if ($description)
                {
                    $current_change{sd1} = $description;
                }
            }
            elsif ( $type eq 'ADD' && keys %current_change )
            {
                my $display_name = extract_label( 'DisplayName', $change_text );
                if ($display_name)
                {
                    $current_change{dn2} = $display_name;
                }

                my $description = extract_label( 'ShortDescription', $change_text );
                if ($description)
                {
                    $current_change{sd2} = $description;
                }

                # setup digests
                my $add_this_change = '';
                if ( $current_change{dn1} && $current_change{dn2} && $current_change{dn1} ne $current_change{dn2} )
                {
                    $current_change{dn1_digest} = label_digest( $current_change{dn1} );
                    $current_change{dn2_digest} = label_digest( $current_change{dn2} );
                    unless ( $current_change{dn1_digest} eq $current_change{dn2_digest} )
                    {
                        $add_this_change = 'true';
                    }
                }
                if ( $current_change{sd1} && $current_change{sd2} && $current_change{sd1} ne $current_change{sd2} )
                {
                    $current_change{sd1_digest} = label_digest( $current_change{sd1} );
                    $current_change{sd2_digest} = label_digest( $current_change{sd2} );
                    unless ( $current_change{sd1_digest} eq $current_change{sd2_digest} )
                    {
                        $add_this_change = 'true';
                    }
                }

                push( @label_changes, {%current_change} ) if $add_this_change;
                %current_change = ();
            }
        }
    }

    # return the changes
    $s->log( "Found " . @label_changes . " label(s) that have changed" );
    return @label_changes;
}

#################### subroutine header begin ####################

=head2 update_system_labels 

 Usage     : update_system_labels($system, \@label_changes);
 Purpose   : updates label if within the system
 Returns   : nothing
 Argument  : system to update, list of changed labels
 Throws    : nothing
 Comment   : 


See Also   :

=cut

#################### subroutine header end ###################

sub update_system_labels
{
    my $system        = shift;
    my $label_changes = shift;

    # got the changes now, go through and update each
    $s->log("Updating label ids in $system DB");
    foreach my $label ( @{$label_changes} )
    {
        if ( $label->{dn1_digest} && $label->{dn2_digest} )
        {
            update_db( $label->{dn1_digest}, $label->{dn2_digest} );
        }
        if ( $label->{sd1_digest} && $label->{sd2_digest} )
        {
            update_db( $label->{sd1_digest}, $label->{sd2_digest} );
        }
    }
}

#################### subroutine header begin ####################

=head2 update_db 

 Usage     : update_db( $label->{dn1_digest}, $label->{dn2_digest});
 Purpose   : replacement done in db
 Returns   : nothing
 Argument  : the two digests
 Throws    : nothing
 Comment   : 

See Also   :

=cut

#################### subroutine header end ###################

sub update_db
{
    my $label_id1 = shift;
    my $label_id2 = shift;

    my $res = $s->{DB}->run_statement(
        {
            string => 'UPDATE zz_labels SET digest = ? WHERE digest = ?',
            params => [ $label_id2, $label_id1, ]
        }
    );
    unless ($res)
    {
        $s->log("Unable to update labels, SQL error");
    }
}

#################### subroutine header begin ####################

=head2 update_label_ids 

 Usage     : update_label_ids( $file_contents, \@label_changes );
 Purpose   : updates label files with any changes
 Returns   : nothing
 Argument  : contents to update, list of changed labels
 Throws    : nothing
 Comment   : 


See Also   :

=cut

#################### subroutine header end ###################

sub update_label_ids
{
    my $file_contents = shift;
    my $label_changes = shift;

    # got the changes now, go through and update each
    $s->log("Updating label ids in file");
    foreach my $label ( @{$label_changes} )
    {
        if ( $label->{dn1_digest} && $label->{dn2_digest} )
        {
            $file_contents =
              replace_in_file( $file_contents, $label->{dn1_digest}, $label->{dn2_digest}, $label->{dn2} );
        }
        if ( $label->{sd1_digest} && $label->{sd2_digest} )
        {
            $file_contents =
              replace_in_file( $file_contents, $label->{sd1_digest}, $label->{sd2_digest}, $label->{sd2} );
        }
    }

    return $file_contents;
}

#################### subroutine header begin ####################

=head2 replace_in_file 

 Usage     : replace_in_file($file, $sd1_digest, $sd2_digest, $label);
 Purpose   : replacement done in file
 Returns   : changed file
 Argument  : file and the two digests and the label which was changed
 Throws    : nothing
 Comment   : 

See Also   :

=cut

#################### subroutine header end ###################

sub replace_in_file
{
    my $file      = shift;
    my $label_id1 = shift;
    my $label_id2 = shift;
    my $label     = shift;

    if ( $file =~ s/$label_id1/$label_id2/g )
    {
        $s->log("Changed id for label: $label");
    }

    return $file;
}

#################### subroutine header begin ####################

=head2 label_digest 

 Usage     : label_digest($label);
 Purpose   : returns the label digest for a label
 Returns   : digest
 Argument  : label
 Throws    : nothing
 Comment   : 

See Also   :

=cut

#################### subroutine header end ###################

sub label_digest
{
    my $label = shift;

    # this is what is done in Zymonic::Base::ll
    return md5_hex( encode_utf8($label) ) . sha256_hex($label);
}

#################### subroutine header begin ####################

=head2 extract_label 

 Usage     : extract_label($type, $change);
 Purpose   : extras the label $type from the $change
 Returns   : extracted label
 Argument  : type (DisplayName|ShortDescription) and change text
 Throws    : nothing
 Comment   : 

See Also   :

=cut

#################### subroutine header end ###################

sub extract_label
{
    my $type   = shift;
    my $change = shift;

    if ( $change =~ /<$type>(.*)<\/$type>/sm )
    {
        my $name = $1;

        # don't match chunks of XML, would need some manual intervention to do this
        if ( $name =~ /\</ )
        {
            return '';
        }
        else
        {
            return $name;
        }
    }

    return '';
}

