# +-----------------------------------------------------------------------+
# | pLoader - a Perl photo uploader for Piwigo                            |
# +-----------------------------------------------------------------------+
# | Copyright(C) 2008-2010 Piwigo Team                  http://piwigo.org |
# +-----------------------------------------------------------------------+
# | This program is free software; you can redistribute it and/or modify  |
# | it under the terms of the GNU General Public License as published by  |
# | the Free Software Foundation                                          |
# |                                                                       |
# | This program 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.                              |
# |                                                                       |
# | 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., 59 Temple Place - Suite 330, Boston, MA 02111-1307, |
# | USA.                                                                  |
# +-----------------------------------------------------------------------+
package Uploader::PWG::WebServices;
 
use strict;
use warnings;
use MIME::Base64 qw(encode_base64); 
use JSON;
use LWP::UserAgent;
use Data::Dumper;
use Digest::MD5::File qw/file_md5_hex md5_hex/;
use File::Slurp;
use File::Spec;
use POSIX qw(ceil floor);
use base qw/
           Uploader::Object
           Class::Accessor::Fast
           /;

__PACKAGE__->mk_accessors( 
    qw/
           uagent
           urlbase
           username
           password
           qry_list_categories
           qry_add_categories
           qry_list_methods
           qry_list_tags
           qry_check_upload
           items
           tags
           categories
           site_original_filename
           site_high_file
           site_resized_file
           site_thumb_file
           site_image_name
           site_tags
           rank
           site_author
           site_comment
           site_img_date_creation
           uagent_response
           login_result
           action_result
           upload_high
           chunk_size
           sum
           image_id
           typecode
           reupload_action_files
           reupload_action_properties
           reupload_action_properties_m
           single_value_mode
           multiple_value_mode
           privacy_level
           is_method_available
      / 
);

$|=1;

sub Init {
    my ( $self, $version ) = @_ ;

    # to transform a type_code into a typename
    $self->typecode(
        {
             file => 'file',
             thumbnail => 'thumb',
             high => 'high',    
        }
    );
    
    $self->single_value_mode(
        {
            1 => 'fill_if_empty',
            2 => 'replace',
        }
    );

    $self->multiple_value_mode(
        {
            1 => 'append',
            2 => 'replace',
        }
    );
    
    $self->uagent(
        LWP::UserAgent->new(
            agent => sprintf("Mozilla/pLoader %s", $version)        
        )
    );
    
    $self->uagent->cookie_jar({});    

    $self->urlbase(
        $self->{site_url}
    );
    
    $self->username(
        $self->{site_username}
    );
    
    $self->password(
        $self->{site_password}
    );
    
    $self->chunk_size(
        $self->{chunk_size}
    );

    
    $self->uagent->default_headers->authorization_basic(
        $self->{http_username}||$self->username, 
        $self->{http_password}||$self->password
    );
    
    
    $self->qry_list_categories( sprintf
        "%s/ws.php?format=json&method=%s&recursive=%s",
        $self->urlbase,
#        'pwg.categories.getAdminList',
        'pwg.categories.getList',
        'true',
    );

    $self->qry_list_tags( sprintf
        "%s/ws.php?format=json&method=%s",
        $self->urlbase,
        'pwg.tags.getAdminList',
    );

    $self->qry_list_methods( sprintf
        "%s/ws.php?format=json&method=%s",
        $self->urlbase,
        'reflection.getMethodList',
    );

    $self->qry_check_upload( sprintf
        "%s/ws.php?format=json&method=%s",
        $self->urlbase,
        'pwg.images.checkUpload',
    );


    my $form = {
        method => 'pwg.session.login',
        username => $self->username,
        password => $self->password,
    };
 
    $self->_execute_post(
        $form
    );    

    $self->login_result(
        $self->_json_response_content
    );

}

sub _json_response_content {
    my ( $self ) = @_;

    my $hresult = {} ;

    if($self->uagent_response->is_success){
        eval {
            $hresult = from_json(
                $self->uagent_response->content
            );
        };
        if($@){
            # when server response has warnings, the content response is not a valid json string
            # find the json response
            $self->uagent_response->content =~ /(\{.+\})/;
            #printf("json response %s\n", $1);
            eval {
                $hresult = from_json(
                    $1
                );
            };
        }        
    }
    else{
        $hresult = {
            'message' => $self->uagent_response->message,
            'stat'    => 'fail',
        };
    }

    if(401 == $self->uagent_response->code){
        $hresult->{message} = 'Access denied';
    }

    $hresult;
}

sub _execute_get {
    my ( $self, $query ) = @_;

    eval {
        $self->uagent_response(
            $self->uagent->get(
                $query
            )
        );
    };

    if($@){
        printf("An error occured in query execution %s\n%s",
            $query,
            $@,
        );    
    }
}

sub _execute_post {
    my ( $self, $form ) = @_;

    my $result;
    eval {
        $result = $self->uagent_response(
            $self->uagent->post(
                $self->urlbase.'/ws.php?format=json',
                $form
            )
        );
    };
    if($@){
        printf("An error occured in post execution %s\n%s",
            $form->{method},
            $@,
        );    
    }
    
    return ( $result->is_success, $result->status_line );
}

sub GetCategories {
    my ( $self ) = @_;

 
    $self->_execute_get(
        $self->qry_list_categories
    );
    $self->_json_response_content->{result}{categories};
}

sub GetTags {
    my ( $self ) = @_;

    $self->_execute_get(
        $self->qry_list_tags    
    );
    $self->_json_response_content->{result}{tags};
}

sub AddTags {
    my ( $self, $name ) = @_;

    my $form = {
        method            => 'pwg.tags.add',
        name              => $name,
        
    };

    return ( $self->_execute_post($form), $self->_json_response_content );
} 

sub CheckUpload {
    my ( $self ) = @_;

    my $err_msg;

    if($self->is_method_available->{'pwg.images.checkUpload'}){
        $self->_execute_get(
            $self->qry_check_upload
        );
#        my $result = $self->_json_response_content->{result};
        my $hresult = $self->_json_response_content;

        $err_msg = 'ok' eq $hresult->{stat} ?
            $hresult->{result}{message} :
            $hresult->{message};
    }

    $err_msg;
}

sub GetMethods {
    my ( $self ) = @_;

    $self->_execute_get(
        $self->qry_list_methods    
    );

    my $methods = $self->_json_response_content->{result}{methods};

    $self->is_method_available(
        {}
    );

    map {
        $self->is_method_available->{$_}++
    }@$methods;

    $methods;
}

sub UploadImage {
    my ( $self, $progress ) = @_;
    
    return if $progress->{stop_processing};

    $self->image_id(
        $self->_exists($progress->{original_sum})
    );

    my $status = 1;
    my $status_line ="OK";
    my $content = {};
    my $doubleCheck;
    my $form;
    UPLOAD: while(1){
        # first upload
        if(!defined($self->image_id)){ 
            $doubleCheck = 1;
            $self->_checksum_files($progress);
            my @types = ('file', 'thumb');
            #printf("WS upload_high %s\n", $self->upload_high);
            push @types, 'high' if $self->upload_high;
            map{
                my $rval = $self->_send_chunks($_, $progress);
                $status_line = $rval->{message};
                if (!$rval->{ok}){
                    $status = 0;
                    last UPLOAD ;
                }
            } @types;
          
           
            $form = {
                method            => 'pwg.images.add',
                original_sum      => $self->sum->{original},
                original_filename => $self->site_original_filename,
                file_sum          => $self->sum->{file},
                thumbnail_sum     => $self->sum->{thumb},
                categories        => $self->categories,
                name              => $self->site_image_name,
                author            => $self->site_author,
                comment           => $self->site_comment,
                date_creation     => $self->site_img_date_creation,
                tag_ids           => $self->site_tags,
                
            };

           $form->{high_sum} = $self->sum->{high} if $self->upload_high;
           
           $progress->{yield}->();
           ( $status, $status_line ) = $self->_execute_post($form);
        }
        # re-upload
        else{
            # need to check if files have changed
            # and update image info
            if($self->reupload_action_files){
                $self->_checksum_files($progress);
                my $files = $self->_check_files();
                if(defined($files)){
                    $self->_add_files($files, $progress);    
                }
            }

            $form = {
                    method        => 'pwg.images.setInfo',
                    image_id      => $self->image_id,
            };
            # update metadata info
            # simple value metadata
            if($self->reupload_action_properties){      
                $form->{name}          = $self->site_image_name;
                $form->{author}        = $self->site_author;
                $form->{comment}       = $self->site_comment;
                $form->{date_creation} = $self->site_img_date_creation;
                $form->{single_value_mode}  = $self->single_value_mode->{$self->reupload_action_properties};
                $form->{level} = $self->privacy_level ? 2**($self->privacy_level - 1) : 0;
            }
            # multi value metadata
            if($self->reupload_action_properties_m){      
                $form->{tag_ids} = $self->site_tags if $self->site_tags;
                $form->{categories} = $self->categories;
                $form->{multiple_value_mode} = $self->multiple_value_mode->{$self->reupload_action_properties_m};
            };
            $progress->{yield}->();
            ( $status, $status_line ) = $self->_execute_post($form); 

        }

        delete $form->{tag_ids} unless defined $self->site_tags;
        delete $form->{tag_ids} if '' eq $self->site_tags;

        $progress->{yield}->();
        # for first upload
        # make sure the image is uploaded by querying for its existence
        if($doubleCheck){
            $self->image_id(
                $self->_exists($progress->{original_sum})
            );
            $content->{stat} = !defined $self->image_id  ? 'fail' : 'ok'; 
            if(defined $self->image_id and defined $self->privacy_level){
                ( $status, $status_line, $content ) = $self->_set_privacy_level;
            }
        }

        last UPLOAD;
    }# UPLOAD
    
    return ( $status,  $status_line, $content);
}

sub _set_privacy_level {
    my ( $self ) = @_;
        
    my $form = {
        method        => 'pwg.images.setPrivacyLevel',
        image_id      => $self->image_id,
        level         => $self->privacy_level ? 2**($self->privacy_level - 1) : 0,
    };

    my ( $status, $status_line ) = $self->_execute_post($form); 
    my $hresult = $self->_json_response_content;

    ($status, $status_line, $hresult );
}

sub _checksum_files {
    my ( $self, $progress ) = @_;

    $self->sum(
        {
            file => $self->_checksum(
                        $self->site_resized_file,
                        $progress
                    ),
            thumb => $self->_checksum(
                             $self->site_thumb_file,
                             $progress
                         ),
            original => $progress->{original_sum}
        }
    ); 

    $self->sum->{high} = $self->_checksum(
                        $self->site_high_file,
                        $progress
    ) if $self->upload_high ;
}

sub _check_files {
    my ( $self ) = @_;

    my $form = {
        method   => 'pwg.images.checkFiles',
        image_id => $self->image_id,
    };
    
    @$form{'thumbnail_sum', 'file_sum' } = (
        $self->sum->{thumb},
        $self->sum->{file},
    );

    if($self->upload_high){
        $form->{high_sum} = $self->sum->{high};
    }

    $self->_execute_post($form);    
    my $hresult = $self->_json_response_content;

    my $rval = 'ok' eq $hresult->{stat} ? $hresult->{result} : undef;
    
    $rval;
}

# $files is returned by _check_files
# {
#     thumbnail => 'equals', 'differs', 'missing'
#     file => 'equals', 'differs', 'missing'
#     high => 'equals', 'differs', 'missing'
#}

sub _add_files {
    my ( $self, $files, $progress ) = @_;

    map{
        $self->_add_file($_, $progress);
    }
    map{
        $self->typecode->{$_};
    } grep { 'equals' ne $files->{$_} } keys %$files ;
        
}

sub _add_file {
    my ( $self, $type_code, $progress ) = @_;

    $self->_send_chunks(
        $type_code,
        $progress,
    );

    my $form = {
        method => 'pwg.images.addFile',
        image_id => $self->image_id,
        type     => $type_code,
        sum      => $self->sum->{$type_code},        
    };    

    $self->_execute_post($form);    
    my $hresult = $self->_json_response_content;

    my $rval = 'ok' eq $hresult->{stat} ? $hresult->{result} : undef;
    
    $rval;
}

sub IsAlreadyUploaded {
    my ( $self, $md5_sums ) = @_;
    
    # md5_sums is an array ref
    $self->_execute_post({
        method      => 'pwg.images.exist',
        md5sum_list => join(',', @$md5_sums)
        }
    );

    my $sums = $self->_json_response_content->{result};

    $sums;
}

sub _exists {
    my ( $self, $md5_sum ) = @_;

    my $form = {
        method            => 'pwg.images.exist',
        md5sum_list       => $md5_sum,
    };

    $self->_execute_post($form);    
    my $hresult = $self->_json_response_content;

    $hresult->{result} = {} if 'HASH' ne ref $hresult->{result};
    my $id = 'ok' eq $hresult->{stat} ? $hresult->{result}{$md5_sum} : undef ;

    $id;
} 

sub _checksum {
    my ( $self, $file, $progress ) = @_;
    my $file_sum;

    my $yield = $progress->{yield};

    $yield->();
    $progress->{msg_details}->(
        sprintf(
            "%s : %s", 
            $progress->{checksum_msg}, 
            $file
        )
    );

    eval {
        $file_sum = file_md5_hex(
            $file
        );
    };
    $yield->();

    $file_sum;
}

sub _send_chunks {
    my ( $self, $type_code, $progress ) = @_;

    my $msg = {
        thumb=>'thumbnail_msg',
        file=>'resized_msg',
        high=>'highdef_msg',
    };

    my $filepath = {
        thumb=>$self->site_thumb_file,
        file=>$self->site_resized_file,
        high=>$self->site_high_file,
    };

    $progress->{current_msg} = $progress->{$msg->{$type_code}};
    $progress->{yield}->();

    my $params = {
         filepath      => $filepath->{$type_code},
         type          => $type_code,
         original_sum  => $self->sum->{original},
    };
    #print Dumper $params;
    $self->send_chunks(
       $params,
       $progress,
    );
    $progress->{yield}->();

    $params;
}

sub AddCategories{
    my ( $self, $name, $parentid ) = @_;

    my $form = {
        method            => 'pwg.categories.add',
        name              => $name,
        parent            => $parentid,
        
    };
    
    return ( $self->_execute_post($form), $self->_json_response_content );
}

sub SetInfoCategories{
    my ( $self, $name, $comment, $parentid ) = @_;

    my $form = {
        method            => 'pwg.categories.setInfo',
        name              => $name,
        comment           => $comment,
        category_id       => $parentid,
        
    };

    return ( $self->_execute_post($form), $self->_json_response_content );
}


sub send_chunks {
    my ( $self, $params, $progress ) = @_;

    my $yield = $progress->{yield};
    my ( $vol, $dir, $filename ) = File::Spec->splitpath($params->{filepath});

    $yield->();
    $progress->{bar}->(0);
    $yield->();
    $progress->{msg_details}->(
        sprintf(
            "%s : %s", 
            $progress->{current_msg}, 
            $filename
        )
    );


    $yield->();
    my $content = read_file(
        $params->{filepath},
        binmode => ':raw',
    );
    $yield->();

    my $content_length = length($content);
    my $nb_chunks = ceil($content_length / $self->chunk_size->());

    my $chunk_pos = 0;
    my $chunk_id = 1;


    while ($chunk_pos < $content_length) {

        my $chunk = substr(
            $content,
            $chunk_pos,
            $self->chunk_size->()
        );
        $chunk_pos += $self->chunk_size->();
        #print "pwg.images.addChunk\n";
        my $data = encode_base64($chunk);
        #printf("chunk : %s, data %s\n", length $chunk, length $data);
        my $response = $self->uagent->post(
            $self->urlbase.'/ws.php?format=json',
            {
                method       => 'pwg.images.addChunk',
                data         => $data,
                original_sum => $params->{original_sum},
                position     => $chunk_id,
                type         => $params->{type},
            }
        );
#print Dumper $response;
        $yield->();
        $progress->{bar}->(100*($chunk_pos/$content_length));
        $progress->{msg_details}->(
            sprintf(
                "%s : %s", 
                $progress->{current_msg}, 
                $filename
            )
        );
        $params->{ok} = 1;
        if ($response->code != 200) {
            printf("response code    : %u\n", $response->code);
            printf("response message : %s\n", $response->message);
            $params->{ok} = 0;
            $params->{message} = $response->message;
            $params->{code} = $response->code;
            last;
        }

        $chunk_id++;
    }
}

 
1;
    