package DBIx::dbMan;

=comment

	dbMan 0.47
	(c) Copyright 1999-2023 by Milan Sorm, sorm@is4u.cz
	All rights reserved.

	This software provides some functionality in database managing
	(SQL console).

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

=cut

use strict;
use DBIx::dbMan::Config;     # configuration handling package
use DBIx::dbMan::Lang;       # I18N package - EXPERIMENTAL
use DBIx::dbMan::DBI;        # dbMan DBI interface package
use DBIx::dbMan::MemPool;    # dbMan memory management system package
use Data::Dumper;

our $VERSION = '0.47';

# constructor, arguments are hash of style -option => value, stored in internal attributes hash
sub new {
    my $class = shift;
    my $obj = bless { @_ }, $class;
    return $obj;
}

# main loop of dbMan life-cycle, called from exe file
sub start {
    my $obj = shift;    # main dbMan core object

    $obj->{ -trace } = $ENV{ DBMAN_TRACE } || 0;    # standard extension tracing activity - DISABLED

    # what interface exe file want ??? making package name from it
    my $interface = $obj->{ -interface };
    $interface = 'DBIx/dbMan/Interface/' . $interface . '.pm';

    # we try to require interface package - found in @INC, syntax check,
    # load it by require instead of use because we know only filename
    eval { require $interface; };
    if ( $@ ) {    # if something goes wrong
        $interface =~ s/\//::/g;
        $interface =~ s/\.pm$//;

        # bad information for user :-(
        print STDERR "Can't locate interface module $interface\n";
        return;    # see you later...
    }

    # making class name from interface package filename
    $interface =~ s/\//::/g;
    $interface =~ s/\.pm$//;

    # creating memory management object - mempool
    $obj->{ mempool } = new DBIx::dbMan::MemPool;

    # creating configuration object
    $obj->{ config } = new DBIx::dbMan::Config;

    # creating I18N specifics object with configuration object as argument
    $obj->{ lang } = new DBIx::dbMan::Lang -config => $obj->{ config };

    # creating loaded interface object, all objects as arguments
    # included dbMan core object
    $obj->{ interface } = $interface->new(
        -config => $obj->{ config },
        -lang   => $obj->{ lang }, -mempool => $obj->{ mempool }, -core => $obj
    );

    # we have interface now, we can produce messages and errors by object
    # method $obj->{interface}->print('what we can say to user...')

    # dbMan interface, please introduce us to our user (welcome message, splash etc.)
    $obj->{ interface }->hello();

    # creating dbMan DBI object - encapsulation of DBI with multiple connections
    # support, configuration, interface and mempool as arguments
    $obj->{ dbi } = new DBIx::dbMan::DBI -config => $obj->{ config },
        -interface => $obj->{ interface }, -mempool => $obj->{ mempool };

    # looking for and loading all extensions
    $obj->load_extensions;

    # we say to the interface that extensions are loaded and menu can be build
    $obj->{ interface }->rebuild_menu();

    # main loop derived by interface - get_action & handle_action calling cycle
    # NOT CALLED if we are in $main::TEST mode (tested initialization from make test)
    $obj->{ interface }->loop() unless defined $main::TEST && $main::TEST;

    # unloading all loaded extensions
    $obj->unload_extensions;

    # close all opened DBI connections by dbMan DBI object
    $obj->{ dbi }->close_all();

    # dbMan interface, please say good bye to our user...
    $obj->{ interface }->goodbye();

    # test result OK if we are in $main::TEST mode (tested initialization from make test)
    $main::TEST_RESULT = 1 if defined $main::TEST && $main::TEST;

    # program must correctly exit if we want 'test ok' for make test' tests
    exit if $main::TEST_RESULT;
}

# looking for and loading extensions
sub load_extensions {
    my $obj = shift;    # main dbMan core object

    $obj->{ extensions } = [];    # currently loaded extensions = no extensions

    # 1st phase : candidate searching algorithm
    my %candidates = ();          # what are my candidates for extensions ?
    for my $dir ( $obj->extensions_directories ) {    # all extensions directories
        opendir D, $dir;                              # search in directory
        for ( grep /\.pm$/, readdir D ) {             # for each found package
            eval { require "$dir/$_"; };              # try to require
            next if $@;                               # not candidate if fail
            s/\.pm$//;                                # make class name from filename
            my $candidate = "DBIx::dbMan::Extension::" . $_;

            # search for extension version limit (class method) - low and high
            my ( $low, $high ) = ( '', '' );
            eval { ( $low, $high ) = $candidate->for_version(); };

            # not candidate if our version isn't between low and high
            # we must delete filename from include list
            if ( ( $low and $VERSION < $low ) or ( $high and $VERSION > $high ) ) { delete $INC{ "$dir/$_.pm" }; next; }

            # fetching identification from extension (class method)
            my $id = '';
            eval { $id = $candidate->IDENTIFICATION(); };

            # not candidate if identification not specified
            unless ( $id or $@ ) { delete $INC{ "$dir/$_.pm" }; next; }

            # parsing identification AUTHOR-MODULE-VERSION
            my ( $ident, $ver ) = ( $id =~ /^(.*)-(.*)$/ );

            # not candidate if AUTHOR-MODULE isn't overloaded
            if ( $ident eq '000001-000001' ) { delete $INC{ "$dir/$_.pm" }; next; }

            # deleting filename from include list
            delete $INC{ "$dir/$_.pm" };

            # not candidate if exist this identification with same or higher version
            next if exists $candidates{ $ident } && $candidates{ $ident }->{ -ver } >= $ver;

            # save candidate to candidates list
            $candidates{ $ident } = { -file => "$dir/$_.pm", -candidate => $candidate, -ver => $ver };
        }

        closedir D;    # close searched directory
    }

    # 2nd phase : candidate loading algorithm
    my %extensions = ();    # all objects of extensions

    $obj->{ extension_iterator } = 0;    # randomize iterator
    for my $candidate ( keys %candidates ) {    # for each candidate
        my $ext = undef;                        # undefined extension
        eval {                                  # try require file and create object
            require $candidates{ $candidate }->{ -file };

            # object pass all five instances of base objects as argument
            $ext = $candidates{ $candidate }->{ -candidate }->new(
                -config    => $obj->{ config },
                -interface => $obj->{ interface },
                -dbi       => $obj->{ dbi },
                -core      => $obj,
                -mempool   => $obj->{ mempool }
            );

            die unless $ext->load_ok();
        };
        if ( defined $ext and not $@ ) {    # successful loading ?
            my $preference = 0;             # standard preference level
            eval { $preference = $ext->preference(); };    # trying to fetch preference

            # sorting criteria are: preference, random iterator
            # saving sort criteria for later using
            $ext->{ '___sort_criteria___' } = $preference . '_' . $obj->{ extension_iterator };

            # save instance of object to hash indexed by preference
            $extensions{ $preference . '_' . $obj->{ extension_iterator } } = $ext;

            ++$obj->{ extension_iterator };    # increase random iterator
        }
    }

    # 3rd phase : building candidates list sorted by preference (for action handling)
    for (
        sort {                                 # sorting criteria - first time by preference, second time loading order
            my ( $fa, $sa, $fb, $sb ) = split /_/, $a . '_' . $b;
            ( $fa == $fb ) ? ( $sa <=> $sb ) : ( $fb <=> $fa );
        } keys %extensions
        ) {                                    # for all loaded extensions

        # save extension into sorted list
        push @{ $obj->{ extensions } }, $extensions{ $_ };

        # call init() for initializing extension (all extensions in correct order)
        $extensions{ $_ }->init();
    }

    # all extensions are loaded and sorted by preference into $obj->{extensions} list
}

# unloading all extensions
sub unload_extensions {
    my $obj = shift;    # main dbMan core object

    for ( @{ $obj->{ extensions } } ) {    # for all extensions in standard order
        $_->done();                        # call done() for finalizing extension
        undef $_;                          # destroy extension instance of object
    }
}

# produce list of all extensions directories
sub extensions_directories {
    my $obj = shift;                       # main dbMan core object

    # grep criteria - only directories which contains DBIx/dbMan/Extension subfolder are wanted
    # tested dirs are: @INC, extensions_dir configuration directive, current folder
    # WARNING: i must call extensions_dir in list context if I want list of directories
    return grep { -d $_ } map { my $t = $_; $t =~ s/\/$//; "$t/DBIx/dbMan/Extension" } ( @INC, ( $obj->{ config }->extensions_dir ? ( $obj->{ config }->extensions_dir ) : () ), '.' );
}

# show tracing record via interface object
sub trace {
    my ( $obj, $direction, $where, %action ) = @_;    # main dbMan core object,
                                                      # direction string (passed to interface), extension object and action record

    # change $where to readable form
    $where =~ s/=.*$//;
    $where =~ s/^DBIx::dbMan::Extension:://;
    my $params = '';
    for ( sort keys %action ) {                       # for all actions
        next if $_ eq 'action';                       # action tag ignore
        my $p = $action{ $_ };
        $p = "'$p'" if $p !~ /^[-a-z0-9_.]+$/i;       # stringify
        $params .= ", " if $params;
        $params .= "$_: $p";                          # concat
    }

    # change non-selected chars in $params to <hexa> style
    $params = join '',                                # joining transformed chars
        map { ( $_ >= 32 && $_ != 255 && $_ != 127 ) ? chr : sprintf "<%02x>", $_; } unpack "C*", $params;    # disassemble $params into chars

    # sending tracing report via interface object
    $obj->{ interface }->trace( "$direction $where / $action{action} / $params\n" );
}

# main loop for handling one action
sub handle_action {
    my ( $obj, %action ) = @_;                                                                                # main dbMan core object, action to process

    $action{ processed } = undef;                                                                             # save signature of old action for deep recursion test
    my $oldaction = \%action;

    for my $ext ( @{ $obj->{ extensions } } ) {                                                               # going down through all extensions in preference order
        $action{ processed } = 1;
        last if $action{ action } eq 'NONE';                                                                  # stop on NONE actions

        my $acts = undef;
        eval { $acts = $ext->known_actions; };                                                                # hack - which actions extension want ???
        next
            if $@
            || ( defined $acts
            && ref $acts eq 'ARRAY'
            && ! grep { $_ eq $action{ action } } @$acts );                                                   # use hacked knowledge

        $obj->trace( "<==", $ext, %action ) if $obj->{ -trace };                                              # trace if user want

        $action{ processed } = undef;                                                                         # standard behaviour - action not processed
        eval { %action = $ext->handle_action( %action ); };                                                   # handling action
        if ( $@ && $@ !~ /^Catched signal INT/ ) {                                                                                           # error - exception
            $obj->{ interface }->error( "Exception catched: $@" );
            $action{ processed } = 1;
            $action{ action }    = 'NONE';
        }

        $obj->trace( "==>", $ext, %action ) if $obj->{ -trace };                                              # trace if user want

        last unless $action{ processed };                                                                     # action wasn't processed corectly
                                                                                                              # ... prefix probably set - return to get_event (and called once again we hope)
    }

    $obj->{ -deep_detected } = 0;

    # deep recursion detection
    unless ( $action{ processed } ) {
        my $newaction = \%action;
        if ( $obj->compare_struct( $oldaction, $newaction ) ) {
            if ( $obj->{ -deep_detected } >= 100 ) {
                $obj->trace( "Deep recursion detected...\n", '- new:', %action );
                $obj->trace( "",                             '- old:', %$oldaction );
                $action{ processed } = 1;
            }
            else {
                ++$obj->{ -deep_detected };
            }
        }
    }

    # action processed correctly, good bye with modified action record
    return %action;
}

# return 1 if structs are identical
sub compare_struct {
    my $obj = shift;
    my ( $a, $b ) = @_;

    my $first  = Data::Dumper->Dump( [ $a ] );
    my $second = Data::Dumper->Dump( [ $b ] );
    return $a eq $b;

    return 0;
}

1;    # all is O.K.
