# Copyright (c) 2004  Joshua Hoblitt
#
# $Id: Server.pm,v 1.1.1.1 2004-12-22 02:16:24 jhoblitt Exp $

package PS::IPP::IData::Server;

use strict;
use warnings;
no warnings qw( uninitialized );

our $VERSION = '0.01';

use DBI;
use PS::IPP::IData::Util qw( :standard );
use PS::IPP::IData::Server::Log;
use PS::IPP::IData::Server::SQL;
use Log::Log4perl;
use Params::Validate qw( validate_pos SCALAR );

my %class = (
    raw => 1,
);

my $log;
my $sql;
my $db;

sub setup {
    my $self = shift;

    my ( $dsn, $user, $passwd ) = validate_pos( @_,
        {
            type        => SCALAR,
        },
        {
            type        => SCALAR,
        },
        {
            type        => SCALAR,
        },
    );

    $log = Log::Log4perl::get_logger( "PS::IPP::IData::Server" );
    $sql = PS::IPP::IData::Server::SQL->new;

    $log->debug( "entered - @_" );

    eval {
        $db = DBI->connect(
            $dsn,
            $user,
            $passwd,
            {
                RaiseError => 1,
                PrintError => 0,
                AutoCommit => 0,
            },
        );

        $db->do( $sql->set_transaction_model );

        $db->commit;
    };
    if ( $@ ) {
        $db->rollback if $db;
        $log->logdie( "database error: $@" );
    }

    $log->debug( "connected to database: ", sub { $db->data_sources; } );

    $log->debug( "leaving" );

    return 1;
}

sub create_object {
    my $self = shift;

    $log->debug( "entered - @_" );

    my ( $key, $class, $volume, $comment ) = validate_pos( @_,
        {
            type        => SCALAR,
        },
        {
            type        => SCALAR,
            callbacks   => {
                'is + integer' => sub { $_[0] =~ /^\d+$/ },
            },
            default     => 0,
        },
        {
            type        => SCALAR,
            optional    => 1,
        },
        {
            type        => SCALAR,
            optional    => 1,
        },
    );

    # TODO do some intelligent volume allocated 
    # add hooks to make this decision based on avaiable space
    $volume = _get_storage_volume( $volume );

    # check that the comment isn't too long
    # check that the class requested is valid
    # check that the volume requested is valid

    my $uri;
    my $ins_id;

    eval {
        {
            # create storage_object
            my $query = $db->prepare( $sql->new_object ); 
            $query->execute( $key, $class, $comment );
        }

        {
            # create instance with no URI
            my $query = $db->prepare( $sql->new_object_instance );
            $query->execute;
        }

        {
            # get instance ID
            my $query = $db->prepare( $sql->last_insert_id );
            $query->execute;
            ( $ins_id ) = $query->fetchrow_array;
        }
    };
    if ( $@ ) {
        $db->rollback;
        $log->logdie( "database error: $@" );
    }

    $uri = "$volume/$key.$ins_id";

    $log->debug( "generated uri $uri");

    my $path = _get_file_path( $uri );

    # TODO add some stuff here to retry if unsucessful
    eval {
        _create_empty_file( $path );
    };
    if ( $@ ) {
        $db->rollback;
        $log->logdie( $@ );
    }

    eval {
        {
            my $query = $db->prepare( $sql->update_instance_uri );
            $query->execute( $uri, $ins_id );
        }

        $db->commit;
    };
    $log->logdie( "database error: $@" ) if $@;

    $log->debug( "leaving" );

    return $uri;
}

sub replicate_object {
    my $self = shift;

    $log->debug( "entered - @_" );

    my ( $key, $volume ) = validate_pos( @_,
        {
            type        => SCALAR,
        },
        {
            type        => SCALAR,
            optional    => 1,
        },
    );

    # TODO
    $volume = _get_storage_volume();

    my $path;
    my $uri;

    my $so_id;
    my $ins_id;
    eval {
        {
            my $query = $db->prepare( $sql->get_object_instances );
            my $rows = $query->execute( $key );

            unless ( $rows > 0 ) {
                $db->rollback;
                $log->logdie( "storage object does not exist" );
            }

            $so_id = $query->fetchrow_hashref->{ 'so_id' };
        }

        {
            my $query = $db->prepare( $sql->new_instance );
            $query->execute( $so_id );
        }

        {
            my $query = $db->prepare( $sql->last_insert_id );
            $query->execute();

            ( $ins_id ) = $query->fetchrow_array;
        }

        {
            $uri = "$volume/$key.$ins_id";
            $log->debug( "generated uri $uri");

            my $query = $db->prepare( $sql->update_instance_uri );
            $query->execute( $uri, $ins_id );
        }
    };
    if ( $@ ) {
        $db->rollback;
        $log->logdie( "database error: $@" );
    }


    eval {
        $path = _get_file_path( $uri );
        $path = _create_empty_file( $path );
    };
    if ( $@ ) {
        $db->rollback;
        $log->logdie( $@ );
    }

    $db->commit;

    $log->debug( "leaving" );

    return $uri;
}

sub lock_object {
    my $self = shift;

    $log->debug( "entered - @_" );

    my ( $key, $type ) = validate_pos( @_,
        {
            type        => SCALAR,
        },
        {
            type        => SCALAR,
            callbacks   => {
                'is read or write' => sub { $_[0] =~ /^(?:read|write)$/ },
            },
        },
    );

    my $so_id;
    my $read_lock;
    my $write_lock;

    eval {
        # this will set update locks
        my $query = $db->prepare( $sql->get_object_locks );
        my $rows = $query->execute( $key );
        unless ( $rows == 1 ) {
            $log->logdie( "storage object does not exist" );
        }

        my $row = $query->fetchrow_hashref;

        $so_id      = $row->{ 'so_id' };
        $read_lock  = $row->{ 'read_lock' };
        $write_lock = $row->{ 'write_lock' };
    };
    if ( $@ ) {
        $db->rollback;
        $log->logdie( "database error: $@" );
    }

    if ( $type eq 'write' ) {
        # can't set a write lock twice and
        # can't set a write lock if there are read locks
        if ( $write_lock ) {
            eval { $db->rollback; };
            $log->logdie( "database error: $@" ) if $@;

            $log->logdie( "can not write lock twice" );
        }
            
        if ( $read_lock > 0 ) {
            eval { $db->rollback; };
            $log->logdie( "database error: $@" ) if $@;

            $log->logdie( "can not write lock after read lock" );
        }

        eval {
            my $query = $db->prepare( $sql->set_write_lock );
            my $rows = $query->execute( $key );
        
            # if we affected more then one row something very bad has happened.
            unless ( $rows == 1 ) {
                $log->logdie( "affected row count is $rows instead of 1" );
            }

            $db->commit;
        };
        if ( $@ ) {
            $db->rollback;
            $log->logdie( "database error: $@" );
        }

        $log->debug( "leaving" );

        return 1;
    }

    if ( $type eq 'read' ) {
        # can't set a read lock if there's a write lock
        if ( $write_lock ) {
            eval { $db->rollback; };
            $log->logdie( "database error: $@" ) if $@;

            $log->logdie( "can not read lock after write lock" );
        }

        eval {
            my $query = $db->prepare( $sql->increment_read_lock );
            my $rows = $query->execute( $key );
        
            # if we affected more then one row something very bad has happened.
            unless ( $rows == 1 ) {
                $log->logdie( "affected row count is $rows instead of 1" );
            }

            $db->commit;
        };
        if ( $@ ) {
            $db->rollback;
            $log->logdie( "database error: $@" );
        }

        $log->debug( "leaving" );

        return 1;
    }

    # never reached
    $log->logdie( "this should never happen" );
}

sub unlock_object {
    my $self = shift;

    $log->debug( "entered - @_" );

    my ( $key, $type ) = validate_pos( @_,
        {
            type        => SCALAR,
        },
        {
            type        => SCALAR,
            callbacks   => {
                'is read or write' => sub { $_[0] =~ /^(?:read|write)$/ },
            },
        },
    );

    my $so_id;
    my $read_lock;
    my $write_lock;

    eval {
        # this will set update locks
        my $query = $db->prepare( $sql->get_object_locks );
        my $rows = $query->execute( $key );
        unless ( $rows == 1 ) {
            $log->logdie( "storage object does not exist" );
        }

        my $row = $query->fetchrow_hashref;

        $so_id      = $row->{ 'so_id' };
        $read_lock  = $row->{ 'read_lock' };
        $write_lock = $row->{ 'write_lock' };
    };
    if ( $@ ) {
        $db->rollback;
        $log->logdie( "database error: $@" );
    }

    if ( $type eq 'write' ) {
        # can't remove a write lock if it doesn't exist
        if ( $read_lock ) {
            eval { $db->rollback; };
            $log->logdie( "database error: $@" ) if $@;
           
            $log->logdie( "can not have a write lock under a read lock" );
        }
        unless ( $write_lock ) {
            eval { $db->rollback; };
            $log->logdie( "database error: $@" ) if $@;

            $log->logdie( "can not remove non-existant write lock" );
        }

        eval {
            my $query = $db->prepare( $sql->delete_write_lock );
            my $rows = $query->execute( $key );
        
            # if we affected more then one row something very bad has happened.
            unless ( $rows == 1 ) {
                $log->logdie( "affected row count is $rows instead of 1" );
            }

            $db->commit;
        };
        if ( $@ ) {
            $db->rollback;
            $log->logdie( "database error: $@" );
        }

        $log->debug( "leaving" );

        return 1;
    }

    if ( $type eq 'read' ) {
        # can't remove a read lock if there's a write lock and
        # can't remove a read lock if there aren't any
        if ( $write_lock ) {
            eval { $db->rollback; };
            $log->logdie( "database error: $@" ) if $@;
           
            $log->logdie( "can not have a read lock under a write lock" );
        }
           
        if ( $read_lock == 0 ) {
            eval { $db->rollback; };
            $log->logdie( "database error: $@" ) if $@;
           
            $log->logdie( "can not remove non-existant read lock" );
        }

        eval {
            my $query = $db->prepare( $sql->decrement_read_lock );
            my $rows = $query->execute( $key );
        
            # if we affected more then one row something very bad has happened.
            unless ( $rows == 1 ) {

                $log->logdie( "affected row count is $rows instead of 1" );
            }

            $db->commit;
        };
        if ( $@ ) {
            $db->rollback;
            $log->logdie( "database error: $@" );
        }

        $log->debug( "leaving" );

        return 1;
    }

    # never reached
    $log->logdie( "this should never happen" );
}


sub find_instances {
    my $self = shift;

    $log->debug( "entered - @_" );

    my ( $key, $volume ) = validate_pos( @_,
        {
            type        => SCALAR,
        },
        {
            type        => SCALAR,
            optional    => 1,
        },
    );

    # TODO restrict search to a volume

    my $query;

    eval {
        $query = $db->prepare( $sql->get_object_instances );
        $query->execute( $key );
    };
    $log->logdie( "database error: $@" ) if $@;

    my @locations;

    while ( my $row = $query->fetchrow_hashref ) {
        my $instance = $row->{ 'uri' };
        push @locations, $instance if $instance;
    }

    $log->logdie( "no instances found" ) unless ( scalar @locations );

    $log->debug( "leaving" );

    return \@locations;
}

sub delete_instance {
    my $self = shift;

    $log->debug( "entered - @_" );

    my ( $uri ) = validate_pos( @_,
        {
            type => SCALAR,
        },
    );

    eval {
        my $so_id;
        my $instances;
        # get so_id
        {
            my $query = $db->prepare( $sql->get_object_from_uri );
            my $rows = $query->execute( $uri );

            unless ( $rows > 0 ) {
                $log->logdie( "no instance is associated with uri" );
            }

            $so_id = $query->fetchrow_hashref->{ 'so_id' };
        }

        {
            my $query = $db->prepare( $sql->get_instance_count );
            $query->execute( $so_id );

            $instances = $query->fetchrow_hashref->{ 'count(ins_id)' };
        }

        # remove instance
        {
            my $query = $db->prepare( $sql->delete_instance );
            my $rows = $query->execute( $uri );
            
            # if we affected something other then one row something very bad has happened
            unless ( $rows == 1 ) {
                $log->logdie( "affected row count is $rows instead of 1" );
            }
        }

        # if we just deleted the last instance associated with a storage object
        # remove it too
        if ( $instances == 1 ) {
            # we just removed the last instance
            my $query = $db->prepare( $sql->delete_object );
            my $rows = $query->execute( $so_id );

            # if we affected something other then one row something very bad has happened
            unless ( $rows == 1 ) {
                $log->logdie( "affected row count is $rows instead of 1" );
            }
        }

        $db->commit;
    };
    if ( $@ ) {
        $db->rollback;
        $log->logdie( "database error: $@" );
    }

    $log->debug( "leaving" );

    return 1;
}

sub stat_object {
    my $self = shift;

    $log->debug( "entered - @_" );

    my ( $key ) = validate_pos( @_,
        {
            type => SCALAR,
        },
    );

    my $stat;
    eval {
        my $query = $db->prepare( $sql->get_object );
        my $rows = $query->execute( $key );

        unless ( $rows == 1 ) {
            $log->logdie( "no storage object found" );
        }

        $stat = $query->fetchrow_arrayref;

        $db->commit;
    };
    if ( $@ ) {
        $db->rollback;
        $log->logdie( "database error: $@" );
    }

    $log->debug( "leaving" );

    return $stat;
}

sub _get_storage_volume {
    $log->debug( "entered - @_" );

    my $name = shift;

    my $volume;
    {
        my $query;
        eval {
            # TODO cache this?
            my $rows;
            if ( $name ) {
                $query = $db->prepare( qq{ SELECT uri FROM volume WHERE name = ? ORDER BY rand() LIMIT 1 } );
                $rows = $query->execute( $name );
            } else {
                $query = $db->prepare( qq{ SELECT uri FROM volume ORDER BY rand() LIMIT 1 } );
                $rows = $query->execute;
            }

            # there has to be atleast one storage volume
            unless ( $rows > 0 ) {
                $db->rollback;

                $log->logdie( "affected row count is $rows instead of 1" );
            } else {
                $db->commit;
            }
        };
        $log->logdie( "database error: $@" ) if $@;

        $volume = $query->fetchrow_hashref->{ 'uri' };
    }

    $log->debug( "leaving" );

    return $volume;
}

1;

__END__
