# -*- perl -*-
##----------------------------------------------------------------------------
## Database Object Interface - ~/lib/DB/Object/Tables.pm
## Version 0.4.1
## Copyright(c) 2020 DEGUEST Pte. Ltd.
## Author: Jacques Deguest <jack@deguest.jp>
## Created 2017/07/19
## Modified 2020/01/19
## All rights reserved
## 
## This program is free software; you can redistribute  it  and/or  modify  it
## under the same terms as Perl itself.
##----------------------------------------------------------------------------
## This package's purpose is to separate the object of the tables from the main
## DB::Object package so that when they get DESTROY'ed, it does not interrupt
## the SQL connection
##----------------------------------------------------------------------------
package DB::Object::Tables;
BEGIN
{
    require 5.6.0;
    use strict;
    use DB::Object::Fields;
    our( $VERSION, $VERBOSE, $DEBUG, @ISA );
    @ISA    = qw( DB::Object );
    $VERSION    = '0.4.1';
    $VERBOSE    = 0;
    $DEBUG      = 0;
    use Devel::Confess;
};

sub init
{
    my $self  = shift( @_ );
    my $table = '';
    $table    = shift( @_ ) if( @_ && @_ % 2 );
    my %arg   = ( @_ );
    return( $self->error( "You must provide a table name to create a table object." ) ) if( !$table && !$arg{table} );
    $table ||= CORE::delete( $arg{table} );
    foreach my $k ( keys( %arg ) )
    {
        $self->{ $k } = $arg{ $k };
    }
    $self->{ 'table' }       = $table if( $table );
    $self->{ 'structure' } ||= {};
    $self->{ 'fields' }    ||= {};
    $self->{ 'default' }   ||= {};
    $self->{ 'null' }      ||= {};
    $self->{ 'types' }       = {};
    $self->{ 'alias' }       = {};
    $self->{ 'avoid' }       = [];
    ## The table type. It could be table or view
    $self->{ 'type' }         = '';
    ## The schema name, if any
    $self->{ 'schema' }         = '';
    $self->{ 'bind' }         = '';
    $self->{ 'cache' }         = '';
    $self->{ 'enhance' }     = '';
    ## Load table default, fields, structure informations
    ## my $db = $self->database();
    my $ref = $self->structure();
    return( $self->error( "There is no table by the name of $table" ) ) if( !%$ref );
    return( $self );
}

##----{ End of generic routines }----##
sub alter
{
    my $self  = shift( @_ );
    ## Expecting a reference to an array
    my $spec  = '';
    $spec     = shift( @_ ) if( @_ == 1 && ref( $_[ 0 ] ) );
    $spec     = [ @_ ] if( @_ && !$spec );
    my $table = $self->{ 'table' } ||
    return( $self->error( "No table was provided." ) );
    return( $self->error( "No proper ALTER specification was provided." ) ) if( !$spec || !ref( $spec ) || !@$spec );
    my $query = "ALTER TABLE $table " . CORE::join( ', ', @$spec );
    my $sth   = $self->prepare( $query ) ||
    return( $self->error( "Error while preparing ALTER query to modify table '$table':\n", $self->errstr() ) );
    if( !defined( wantarray() ) )
    {
        $sth->execute() ||
        return( $self->error( "Error while executing query to ALTER table '$table':\n", $self->as_string(), $sth->errstr() ) );
    }
    return( $sth );
}

sub constant
{
    my $self = shift( @_ );
    my( $pack, $file, $line ) = caller;
    ## $self->message( 3, "Called from package '$pack' in file '$file' at line '$line'." );
    my $base_class = $self->database_object->base_class;
    ## This does not work for calls made internally
    return( $self ) if( $pack =~ /^${base_class}\b/ );
    my $sth = $self->database_object->constant_queries_cache_get({
        pack => $pack,
        file => $file,
        line => $line,
    });
    ## $self->message( 3, "Statement handler returned is: '$sth'." );
    ## $sth returned may be void if no cache was found or if the caller's file mod time has changed
    my $q;
    if( $sth )
    {
        $q = $sth->query_object;
        $self->query_object( $q );
    }
    else
    {
        $q = $self->_reset_query;
    }
    $q->constant({
        sth => $sth,
        pack => $pack,
        file => $file,
        line => $line,
    });
    return( $self );
}

## sub create must be superseded by sub classes
sub create
{
    my $self = shift( @_ );
    my $class = ref( $self );
    return( $self->error( "create() is not implemented by $class." ) );
}

sub create_info
{
    my $self = shift( @_ );
    my $class = ref( $self );
    return( $self->error( "create_info() is not implemented by $class." ) );
}

sub database { return( shift->database_object->database ); }

sub database_object { return( shift->{ 'dbo' } ); }

sub dbh { return( shift->_set_get( 'dbh', @_ ) ); }

sub default
{
    my $self = shift( @_ );
    $self->structure();
    my $default = $self->{ 'default' };
    return( wantarray() ? () : undef() ) if( !%$default );
    return( wantarray() ? %$default : \%$default );
}

sub drop
{
    my $self  = shift( @_ );
    my $table = $self->{ 'table' } || 
    return( $self->error( "No table was provided to drop." ) );
    my $query = "DROP TABLE $table";
    my $sth = $self->prepare( $query ) ||
    return( $self->error( "Error while preparing query to drop table '$table':\n$query", $self->errstr() ) );
    if( !defined( wantarray() ) )
    {
        $sth->execute() ||
        return( $self->error( "Error while executing query to drop table '$table':\n$query", $sth->errstr() ) );
    }
    return( $sth );
}

sub exists
{
    my $self = shift( @_ );
    my $class = ref( $self );
    return( $self->error( "exists() is not implemented by $class." ) );
}

sub fields
{
    my $self = shift( @_ );
    $self->structure();
    my $fields = $self->{fields};
    return( wantarray() ? () : undef() ) if( !%$fields );
    return( wantarray() ? %$fields : \%$fields );
}

sub fields_object
{
    my $self = shift( @_ );
    my $o = $self->{fields_object};
    return( $o ) if( $o && $self->_is_object( $o ) );
    my $db_name = $self->database_object->database;
    $db_name =~ tr/-/_/;
    $db_name =~ s/\_{2,}/_/g;
    $db_name = join( '', map( ucfirst( lc( $_ ) ), split( /\_/, $db_name ) ) );
    my $name = $self->name;
    my $new_class = $name;
    $new_class =~ tr/-/_/;
    $new_class =~ s/\_{2,}/_/g;
    $new_class = join( '', map( ucfirst( lc( $_ ) ), split( /\_/, $new_class ) ) );
    $class = ref( $self ) . "\::${db_name}\::${new_class}";
    unless( $self->_is_class_loaded( $class ) )
    {
        my $perl = <<EOT;
package $class;
BEGIN
{
    use strict;
    use parent qw( DB::Object::Fields );
};

1;

EOT
        # print( STDERR __PACKAGE__, "::_set_get_hash_as_object(): Evaluating\n$perl\n" );
        my $rc = eval( $perl );
        # print( STDERR __PACKAGE__, "::_set_get_hash_as_object(): Returned $rc\n" );
        die( "Unable to dynamically create module $class: $@" ) if( $@ );
    }
    $o = $class->new({
        table_object => $self,
        debug => $self->debug,
    });
    $o->prefixed( $self->{prefixed} );
    $self->{fields_object} = $o;
    return( $o );
}

sub fo { return( shift->fields_object( @_ ) ); }

sub lock
{
    my $self = shift( @_ );
    my $class = ref( $self );
    return( $self->error( "lock() is not implemented by $class." ) );
}

sub name
{
    ## Read-only
    return( shift->{table} );
}

sub null
{
    my $self = shift( @_ );
    $self->structure();
    my $null = $self->{ 'null' };
    return( wantarray() ? () : undef() ) if( !%$null );
    return( wantarray() ? %$null : $null );
}

sub optimize
{
    my $self = shift( @_ );
    my $class = ref( $self );
    return( $self->error( "optimize() is not implemented by $class." ) );
}

sub prefix
{
    my $self = shift( @_ );
    my @val = ();
    CORE::push( @val, $self->database_object->database ) if( $self->{prefixed} > 2 );
    CORE::push( @val, $self->schema ) if( $self->{prefixed} > 1 && $self->schema );
    CORE::push( @val, $self->name ) if( $self->{prefixed} > 0 );
    return( '' ) if( !scalar( @val ) );
    return( CORE::join( '.', @val ) );
}

sub prefix_database { return( shift->{prefixed} > 2 ); }

sub prefix_schema { return( shift->{prefixed} > 1 ); }

sub prefix_table { return( shift->{prefixed} > 0 ); }

## This the prefix intended for field in query
sub prefixed
{
    my $self = shift( @_ );
    if( @_ )
    {
        $self->{prefixed} = ( $_[0] =~ /^\d+$/ ? $_[0] : ( $_[0] ? 1 : 0 ) );
    }
    else
    {
        $self->{prefixed} = 1;
    }
    my $fo = $self->{fields_object};
    $fo->prefixed( $self->{prefixed} ) if( $fo );
    return( $self );
}

sub primary
{
    my $self = shift( @_ );
    $self->structure();
    my $primary = $self->{primary};
    return( wantarray() ? () : undef() ) if( !$primary || !@$primary );
    return( wantarray() ? @$primary : \@$primary );
}

## In PostgreSQL, Oracle, SQL server this would be schema_name.table_name
sub qualified_name { return( shift->name ); }

sub query_object { return( shift->_set_get_object( 'query_object', 'DB::Object::Query', @_ ) ); }

sub query_reset { return( shift->_set_get_scalar( 'query_reset', @_ ) ); }

sub rename
{
    my $self = shift( @_ );
    my $class = ref( $self );
    return( $self->error( "rename() is not implemented by $class." ) );
}

sub repair
{
    my $self = shift( @_ );
    my $class = ref( $self );
    return( $self->error( "repair() is not implemented by $class." ) );
}

sub schema { return( shift->_set_get_scalar( 'schema', @_ ) ); }

sub stat
{
    my $self = shift( @_ );
    my $class = ref( $self );
    return( $self->error( "stat() is not implemented by $class." ) );
}

sub table { return( shift->{table} ); }

sub type { return( shift->_set_get_scalar( 'type', @_ ) ); }

sub types
{
    my $self = shift( @_ );
    $self->structure();
    my $types = $self->{types};
    return( wantarray() ? () : undef() ) if( !%$types );
    return( wantarray() ? %$types : $types );
}

## sub structure must be superseded by sub classes
sub structure
{
    my $self = shift( @_ );
    my $class = ref( $self );
    return( $self->error( "structure() is not implemented by $class." ) );
}

sub unlock
{
    my $self = shift( @_ );
    my $class = ref( $self );
    return( $self->error( "unlock() is not implemented by $class." ) );
}

DESTROY
{
    ## Do nothing
    ## DB::Object::Tables are never destroyed.
    ## They are just gateway to tables, and they are cached by DB::Object::table()
    ## print( STDERR "DESTROY'ing table $self ($self->{ 'table' })\n" );
};

1;

__END__

=encoding utf8

=head1 NAME

DB::Object::Tables - Database Object Interface

=head1 SYNOPSIS

=head1 VERSION

    0.4.1

=head1 DESCRIPTION

=head1 CONSTRUCTOR

=over 4

=item B<new>( %arg )

Creates a new L<DB::Object::Tables> objects.
It may also take an hash like arguments, that also are method of the same name.

=over 8

=item I<debug>

Toggles debug mode on/off

=back

=back

=head1 METHODS

=over 4

=item B<alter>()

=item B<create>()

=item B<create_info>()

=item B<database_object>()

=item B<dbh>()

=item B<default>()

=item B<drop>()

=item B<exists>()

=item B<fields>()

=item B<fields_object>()

=back

=head1 AUTHOR

Jacques Deguest E<lt>F<jack@deguest.jp>E<gt>

=head1 SEE ALSO

L<perl>

=head1 COPYRIGHT & LICENSE

Copyright (c) 2019 DEGUEST Pte. Ltd.

All rights reserved

This program is free software; you can redistribute it and/or modify it under the same terms as Perl itself.

=cut