package FirebirdMaker;

use warnings;
use strict;

use base 'Exporter';
use Carp;
use ExtUtils::MakeMaker;
use File::Basename;
use File::Which ();

use Config;

our @EXPORT_OK = qw( WriteMakefile1 setup_for_ms_gcc setup_for_ms_cl
    setup_for_cygwin
    locate_firebird check_and_set_devlibs alternative_locations
    search_fb_home_dirs search_fb_inc_dirs search_fb_lib_dirs
    locate_firebird_ms registry_lookup read_registry read_data
    save_test_parameters read_test_parameters prompt_for_settings
    prompt_for check_str check_path check_exe check_file help_message
    welcome_msg closing_msg create_embedded_files
    $test_conf $test_mark $use_libfbembed );

our @EXPORT = @EXPORT_OK;

our ( $use_libfbembed );
# Temp file names
our $test_conf = 't/tests-setup.tmp.conf';
our $test_mark = 't/tests-setup.tmp.OK';

# Written by Alexandr Ciornii, version 0.23. Added by eumm-upgrade.
sub WriteMakefile1 {
    my %params       = @_;
    my $eumm_version = $ExtUtils::MakeMaker::VERSION;
    $eumm_version = eval $eumm_version;
    die "EXTRA_META is deprecated" if exists $params{EXTRA_META};
    die "License not specified" if not exists $params{LICENSE};
    if (    $params{AUTHOR}
        and ref( $params{AUTHOR} ) eq 'ARRAY'
        and $eumm_version < 6.5705 )
    {
        $params{META_ADD}{author} = $params{AUTHOR};
        $params{AUTHOR} = join( ', ', @{ $params{AUTHOR} } );
    }
    if ( $params{BUILD_REQUIRES} and $eumm_version < 6.5503 ) {

        #EUMM 6.5502 has problems with BUILD_REQUIRES
        $params{PREREQ_PM} =
          { %{ $params{PREREQ_PM} || {} }, %{ $params{BUILD_REQUIRES} } };
        delete $params{BUILD_REQUIRES};
    }
    delete $params{CONFIGURE_REQUIRES} if $eumm_version < 6.52;
    delete $params{MIN_PERL_VERSION}   if $eumm_version < 6.48;
    delete $params{META_MERGE}         if $eumm_version < 6.46;
    delete $params{META_ADD}           if $eumm_version < 6.46;
    delete $params{LICENSE}            if $eumm_version < 6.31;
    delete $params{AUTHOR}             if $] < 5.005;
    delete $params{ABSTRACT_FROM}      if $] < 5.005;
    delete $params{BINARY_LOCATION}    if $] < 5.005;

    WriteMakefile(%params);
}

#- Helper SUBS ---------------------------------------------------------------#

#-- Subs for OS specific setting

sub setup_for_ms_gcc {

    # Support for MinGW (still experimental, patches welcome!)
    #  ActiveState: cc => V:\absolute\path\to\gcc.exe
    #  Strawberry : cc => gcc
    print "Using MinGW gcc\n";

    # For ActiveState Perl hardwired MinGW path          # other idea?
    my $mingw_path = 'C:\Perl\site\lib\auto\MinGW';

    # Expecting absolute paths in Straberry Perl
    my $mingw_inc = $Config{incpath};

    # For ActiveState Perl is  \include                  # always?
    if ( $mingw_inc eq '\include' ) {
        $mingw_inc = File::Spec->catpath( $mingw_path, $mingw_inc );
    }
    my $mingw_lib = $Config{libpth};

    # For ActiveState Perl is  \lib                      # always?
    if ( $mingw_lib eq '\lib' ) {
        $mingw_lib = File::Spec->catpath( $mingw_path, $mingw_lib );
    }

    $INC .= qq{ -I"$mingw_inc"};

    my $cur_libs      = $Config{libs};
    my $cur_lddlflags = $Config{lddlflags};

    my $lib;
    if ( -f "$FB::LIB/fbclient_ms.lib" ) {
        $lib = "$FB::LIB/fbclient_ms.lib";
    }
    else { $lib = "$FB::LIB/gds32_ms.lib"; }

    # This is ugly :)
    eval "
    sub MY::const_loadlibs {
    '
LDLOADLIBS = \"$lib\" $cur_libs
LDDLFLAGS =  -L\"$mingw_lib\" $cur_lddlflags
    '
} ";
}

sub setup_for_ms_cl {
    my $cur_libs      = $Config{libs};
    my $cur_lddlflags = $Config{lddlflags};

    my $lib;
    if (-f "$FB::LIB/fbclient_ms.lib")
        { $lib = "$FB::LIB/fbclient_ms.lib"; }
    else
        { $lib = "$FB::LIB/gds32_ms.lib"; }

    eval "
    sub MY::const_loadlibs {
    '
LDLOADLIBS = \"$lib\" $cur_libs
LDDLFLAGS  = $cur_lddlflags
    '
} ";
}

sub setup_for_cygwin {
    my $cur_libs      = $Config{libs};
    my $cur_lddlflags = $Config{lddlflags};

    my $dll;
    if ( -f "$FB::HOME/bin/fbclient.dll" ) {
        $dll = "$FB::HOME/bin/fbclient.dll";
    }
    else { $dll = "$FB::HOME/bin/gds32.dll"; }

    eval "
    sub MY::const_loadlibs {
    '
LDLOADLIBS = -Wl,--enable-stdcall-fixup \"$dll\" $cur_libs
LDDLFLAGS =  $cur_lddlflags
    '
} ";
}

#-- Subs used to locate Firebird

=head2 locate_firebird

On *nix like systems try different standard paths.

=cut

sub locate_firebird {

    if ( my $fb_config = File::Which::which('fb_config') ) {
        my $cflags = `fb_config --cflags`;
        my @items = split(/\s+/, $cflags);
        for (@items) {
            if (s/^-I\s*//) {
                $FB::INC = $_;
                last;
            }
        }

        my $libflags = `fb_config --libs`;
        @items = split( /\s+/, $libflags );
        for (@items) {
            if ( s/^-L\s*// ) {
                $FB::LIB = $_;
                last;
            }
        }

        check_and_set_devlibs($FB::LIB);
        return;
    }

    my @bd = search_fb_home_dirs();

    foreach my $dir (@bd) {
        if ( -d $dir ) {

            # File names specific to the Firebird/bin dir
            my @fb_files = qw{fbsql isql-fb isql};
                                           # fbsql not yet! but 'isql' is
                                           # used by Virtuoso and UnixODBC
                                           # That's why Debian ships it as
                                           # isql-fb

            my $found = 0;
            while ( !$found ) {
                my $file = shift @fb_files or last;

                $file = File::Spec->catfile( $dir, 'bin', $file );

                if ( -f $file and -x $file ) {
                    # Located
                    my $out = `echo 'quit;' | $file -z 2>&1`;
                    next unless $out =~ /firebird/si;   # Firebird's isql?

                    check_and_set_devlibs($dir);

                    last;
                }
            }
        }
    }

    return;
}

=head2 check_and_set_devlibs

Check and set global variables for home, inc and lib (?...).

=cut

sub check_and_set_devlibs {
    my $fb_dir = shift;

    $FB::HOME = File::Spec->canonpath($fb_dir);

    $FB::INC = $FB::INC || File::Spec->catdir( $FB::HOME, 'include' );
    $FB::INC = alternative_locations('inc') if !-d $FB::INC;

    $FB::LIB = $FB::LIB || File::Spec->catdir( $FB::HOME, 'lib' );
    $FB::LIB = alternative_locations('lib') if !-d $FB::LIB;

    for my $dir ( split(/ /, $Config{libpth} ), $FB::LIB||() ) {
        if ( -e File::Spec->catfile( $dir, 'libfbembed.so' ) ) {
            $FB::libfbembed_available = 1;
            print "libfbembed.so found in $dir\n";
            last;
        }
    }

    die "libfbembed.so not found\n"
        if $ENV{DBD_FIREBIRD_REQUIRE_EMBEDDED}
            and not $FB::libfbembed_available;

    return;
}

=head2 alternative_locations

Search lib and inc in alternative locations.

=cut

sub alternative_locations {
    my $find_what = shift;

    my @fid = ();
    @fid = search_fb_lib_dirs() if $find_what eq q{lib};
    @fid = search_fb_inc_dirs() if $find_what eq q{inc};

    foreach my $dir ( @fid ) {
        return $dir if -d $dir;
    }

    help_message();
    die "Firebird '$find_what' dir not located!";
}

=head2 search_fb_home_dirs

Common places for the Firebird home dir.

=cut

sub search_fb_home_dirs {

    # Add other standard paths here
    return (
        qw{
          /opt/firebird
          /usr/local/firebird
          /usr/lib/firebird
          /usr
          },
    );
}

=head2 search_fb_inc_dirs

Common places for the Firebird include dir.

=cut

sub search_fb_inc_dirs {

    # Add other standard paths here for include
    return (
        qw{
          /usr/include/firebird
          /usr/local/include/firebird
        },
    );
}

=head2 search_fb_lib_dirs

Common places for the Firebird lib dir.

=cut

sub search_fb_lib_dirs {

    # Add other standard paths here for lib
    return (
        qw{
          /usr/lib/firebird
          /usr/local/lib/firebird
        },
    );
}

=head2 locate_firebird_ms

On Windows use the Registry to locate Firebird.

=cut

sub locate_firebird_ms {

    my $hp_ref = registry_lookup('fb');
    if (ref $hp_ref) {
        $FB::HOME = $FB::HOME || File::Spec->canonpath($hp_ref->[0]);
        $FB::INC  = $FB::INC  || File::Spec->catdir( $FB::HOME, 'include' );
        $FB::LIB  = $FB::LIB  || File::Spec->catdir( $FB::HOME, 'lib' );
    }
}

sub registry_lookup {
    my $what = shift;

    my $reg_data = read_data($what);

    my $value;
    foreach my $rec ( @{$reg_data->{$what}} ) {
        $value = read_registry($rec)
    }

    return $value;
}

sub read_registry {
    my $rec = shift;

    my (@path, $path);
    eval {
        require Win32::TieRegistry;

        $path =
          Win32::TieRegistry->new( $rec->{path} )->GetValue( $rec->{key} );
    };
    if ($@) {
        # TieRegistry fails on this key sometimes for some reason
        my $out = `reg query "$rec->{path}" /v $rec->{key}`;

        ($path) = $out =~ /REG_\w+\s+(.*)/;
    }

    $path =~ s/[\r\n]+//g;

    push @path, $path if $path;

    return wantarray ? @path : \@path;
}

=head2 read_data

Read various default settings from the DATA section of this script.

=cut

sub read_data {
    my $app_alias = shift;

    my %reg_data;
    while (<DATA>) {
        my ($app, $key, $path) = split /:/, $_, 3;
        chomp $path;
        next if $app ne $app_alias;
        push @{ $reg_data{$app} }, { key => $key, path => $path } ;
    }

    return \%reg_data;
}

sub save_test_parameters {
    my ($db_path, $db_host, $user, $pass) = @_;

    open my $t_fh, '>', $test_conf or die "Can't write $test_conf: $!";

    my $test_time = scalar localtime();

    my @record = (
        q(# This is a temporary file used for test setup #),
        q(# The field separator is :=                    #),
        q(# Should be deleted at the end of installation #),
        q(# Init section ------ (created by Makefile.PL) #),
        q(# Time: ) . $test_time,
    );

    $db_host = $db_host || q{localhost}; # not ||= for compatibility

    # Other settings (interactive mode)
    push @record, qq(host:=$db_host);
    push @record, qq(path:=$db_path) if $db_path;
    push @record, qq(tdsn:=dbi:Firebird:db=$db_path;host=$db_host;ib_dialect=3;ib_charset=UTF8) if $db_path;
    push @record, qq(user:=$user) if $user;
    push @record, qq(pass:=$pass) if $pass;
    push @record, qq(use_libfbembed:=1) if $use_libfbembed;

    my $rec = join "\n", @record;

    print {$t_fh} $rec, "\n";

    close $t_fh or die "Can't close $test_conf: $!";

    # Remove the mark file
    if (-f $test_mark) {
        unlink $test_mark or warn "Could not unlink $test_mark: $!";
    }

    return;
}

sub read_test_parameters {

    my $record = {};

    if (-f $test_conf) {
        print "\nReading cached test configuration...\n";

        open my $file_fh, '<', $test_conf
            or croak "Can't open file ", $test_conf, ": $!";

        foreach my $line (<$file_fh>) {
            next if $line =~ m{^#+};         # skip comments

            my ($key, $val) = split /:=/, $line, 2;
            chomp $val;
            $record->{$key} = $val;
        }

        close $file_fh;
    }

    return $record;
}

#-- Prompting subs ...

sub prompt_for_settings {

    my $param = read_test_parameters();

    my ($user, $pass) = (qw{SYSDBA masterkey}); # some defaults
    my ($db_path, $db_host);

    # If saved configs exists set them as defaults
    if ( ref $param ) {
        $user = $param->{user} || $user;
        $pass = $param->{pass} || $pass;
        $db_host = $param->{host} || 'localhost';
        $db_path = $param->{path}
          || File::Spec->catfile( File::Spec->tmpdir(), 'dbd-fb-testdb.fdb' );
    }

    print qq{\nStarting interactive setup, two attempts for each option,\n};
    print qq{ if both fail, the script will abort ...\n};
    print qq{\n Enter the full paths to the Firebird instalation:\n};
    $FB::HOME = prompt_for( 'path', '      Home:', $FB::HOME );

    $FB::INC = $FB::INC || File::Spec->catdir( $FB::HOME, 'include' );
    $FB::LIB = $FB::LIB || File::Spec->catdir( $FB::HOME, 'lib' );

    $FB::INC = prompt_for( 'path', '   Include:', $FB::INC );
    $FB::LIB = prompt_for( 'path', '       Lib:', $FB::LIB );

    print qq{\n Configuring the test environment ...\n};

    $db_host = prompt_for('str', '  Hostname:', $db_host );

    print
      qq{\n Enter the full path and file name of the test database (.fdb):\n};
    $db_path = prompt_for( 'file', '   Test DB:', $db_path );

    unless ($use_libfbembed) {
        print qq{\n Enter authentication options:\n};
        $user = prompt_for('str', '   Username:', $user );
        $pass = prompt_for('str', '   Password:', $pass );
        print "\n";
    }

    save_test_parameters($db_path, $db_host, $user, $pass);

    return;
}

=head2 prompt_for

Show prompt.

=cut

sub prompt_for {
    my ( $type, $msg, $value ) = @_;

  LOOP: {
        for ( 1 .. 2 ) {
            $value = prompt( $msg, $value );
            $value = File::Spec->canonpath($value)
              if ( $type eq q{path} or $type eq q{exe} );

            my $check_sub = qq{check_$type};
            last LOOP if ( main->$check_sub($value) );
        }
        die "Unable to locate $type. Aborting ...";
    }

    return $value;
}

sub check_str  { return ( $_[1] ) }
sub check_path { return ( -d $_[1] ) }
sub check_exe  { return ( -x $_[1] ) }

=head2 prompt_new_file

Because we can't make difference between a simple path and a path with
a file name without extension, the fdb extension is required for the
test database.

=cut

sub check_file {
    my ($self, $value) = @_;

    my ($base, $db_path, $type) = fileparse($value, '\.fdb' );

    return 0 if $type ne q{.fdb}; # expecting file with fdb extension

    return ( -d $db_path and $base );
}

#-- Help and message subs

sub help_message {

    my $msg =<<"MSG";

This script prepares the installation of the DBD::Firebird module,
automatically with minimum user intervention or in interactive mode.
In non interactive mode will try to determine the location of the
Firebird HOME, LIBRARY and INCLUDE directories:

1. From the environment variable FIREBIRD_HOME. Also FIREBIRD_INCLUDE
and FIREBIRD_LIB if they are not sub directories of FIREBIRD_HOME.

2. From the standard (hardwired) locations where Firebird can be
installed on various platforms and distros.

If no success, execute this script with the I<-i[nteractive]> command
line option, or set the required environment variables.

% perl Makefile.PL -i[nteractive]

The tests requires the path to the test database, the user name and
the password.  All options have defaults: DBI_USER = 'SYSDBA',
DBI_PASS = 'masterkey', or run the script in interactive
mode. (ISC_USER and ISC_PASSWORD are recognized also), for DBI_DSN the
default is:

  dbi:Firebird:db=OS_tmp_path/dbd-fb-testdb.fdb;host=localhost;
      ib_dialect=3;ib_charset=UTF8

If all else fails, email <mapopa\@gmail.com> for help.

MSG

    print $msg;
}

sub welcome_msg {

    my $msg =<<"MSG";

This script prepares the installation of the DBD::Firebird module.

Warning: the process will create a temporary file to store the values
required for the testing phase, including the password for access to
the Firebird server in plain text: 't/tests-setup.tmp.conf'.

MSG

    print $msg;
}

sub closing_msg {

    my $msg =<<"MSG";

Please, set at least DBI_PASS (or ISC_PASSWORD), before 'make test'.
The default for DBI_USER is 'SYSDBA'.

MSG

    print $msg unless $use_libfbembed;
}

sub copy_mangled {
    my ( $src, $p ) = @_;

    my $dir = 'embed';

    my $df = $p->{new_path} || File::Spec->catfile( $dir, $p->{name} || $src );
    open( my $dfh, '>', $df )  or die "Unable to open $df for writing: $!\n";
    open( my $sfh, '<', $src ) or die "Unable to open $src: $!\n";

    my ($prefix, $skip_shebang);
    if ( $src =~ /\.(?:xs|[ch])$/ ) {
        $prefix = '//';
        $skip_shebang = 0;
    } elsif ( $src =~ /\.pl$/i ) {
        $prefix = '#';
        $skip_shebang = 1;
    }
    else {
        $prefix = '#';
        $skip_shebang = 0;
    }

    my $header_warning = sub {
        my $line = '*' x 60;
        print $dfh "\n" if $skip_shebang;
        print $dfh "$prefix $line\n";
        print $dfh "$prefix This is an automaticaly generated file.\n";
        print $dfh "$prefix If needed, edit $src in the parent directory\n";
        print $dfh "$prefix and run perl Makefile.PL to re-generate it.\n";
        print $dfh "$prefix $line\n\n";
    };

    my $line_no = 0;
    while ( defined( $_ = <$sfh> ) ) {
        &$header_warning if $line_no++ == $skip_shebang;

        last if $p->{last} and &{ $p->{last} }($_);
        &{ $p->{mangle} }($_) if $p->{mangle};
        print $dfh $_;
    }
    close($dfh) or die "Error closing $df: $!\n";
    close($sfh)  or die "Error closing $src: $!\n";
}

sub create_embedded_files {
    my $dir = "embed";

    unless (-d $dir) {
        mkdir($dir) or die "Error creating directory $dir: $!\n";
    }

    # Makefile.PL
    copy_mangled(
        'Makefile.PL' => {
            last   => sub { $_[0] =~ /^exit 0/ },
            mangle => sub { $_[0] =~ s/(?<=^our \$EMBEDDED = )0/1/ },
        }
    );

    # Simple copies
    for my $f (qw( dbdimp.h )) {
        copy_mangled($f);
    }
    copy_mangled(
        'Firebird.h' => {
            name => 'FirebirdEmbedded.h',
        },
    );

    # dbdimp.c
    copy_mangled(
        'dbdimp.c' => {
            mangle =>
                sub { $_[0] =~ s/(?<=^#include "Firebird)\.h"/Embedded.h"/ },
        },
    );

    my $next_is_last = 0;
    copy_mangled(
        'Firebird.pm' => {
            name => 'FirebirdEmbedded.pm',
            last => sub {
                return 1 if $next_is_last;
                if ( $_[0] =~ /^=head1 DESCRIPTION$/ ) {
                    $next_is_last = 1;
                    $_[0] .= <<EOT;


B<DBD::FirebirdEmbedded> is a variant of L<DBD::Firebird>, linked with the
Firebird embedded library, F<libfbembed>. In addition to the ability to work
with remote Firebird servers (which DBD::Firebird has, being linked with the
Firebird client library, F<libfbclient>), DBD::FirebirdEmbedded can be used to
work with Firebird databases without the need of a dedicated Firebird server.

The following things should be set up first:

=over

=item Username/password

These should be unset. Both in the C<< DBI->connection(...) >> call and in the
environment (C<ISC_USER>, C<ISC_PASSWORD>, C<DBI_USER>, C<DBI_PASSWORD> variables).

=item Firebird lock directory

The C<FIREBIRD_LOCK> environment variable should be set to some place where the
process can write. Note that if you plan for several processes to access the
database file directly, they all should have C<FIREBIRD_LOCK> set to the same
directory, or else database corruption will occur.

=item No C<host> in the DSN

Obviously, do not set any host when calling C<< DBI->connection(...) >>, not
even C<localhost>.

=item Permissions to read/write the database

Obviously, the process needs to be able to read/write the database file.

=back

=head1 COMPARISON WITH DBD::FIREBIRD

DBD::FirebirdEmbedded provides exactly the same functionality as the Firebird
server of the same version as the F<libfbembed> library. It still can work with
remote datases, in which case the use is exactly the same (DSN, environment) as
with the regular L<DBD::Firebird>.

=head2 Pros

=over

=item Standalone work with Firebird databases

No need to setup/administer a Firebird server. All the server functionality is
available via the F<libfbembed> library. Shared access to databases is still
possible (but read L</Firebird lock directory> above).

=item No network latency

Working directly with the database file elliminates possible network delays
(even if the server is on the local host).

=back

=head2 Cons

=over

=item Memory footprint

The F<libfbembed> library contains a fully functional Firebird server and is
therefore bigger than the ordinary client library, F<libfbclient>.

=item Setup complexity

It is very important to make sure that all processes that access a given
database use the same lock directory. See L</Firebird lock directory> above.

=back

=head1 SEE ALSO

=over

=item L<DBD::Firebird>

=back

EOT
                }
                return 0;
                },
            mangle => sub {
                $_[0] =~ s/DBD::Firebird - DBI driver for.+/DBD::FirebirdEmbedded - embedded Firebird server (and client)/;
                $_[0] =~ s/dbi:Firebird:.+/dbi:FirebirdEmbedded:db=\$dbname", undef, undef);/g;
                $_[0] =~ s/(?<!L\<)DBD::Firebird\b(?!::(?:Get|Type|Table)Info)/DBD::FirebirdEmbedded/g;
                $_[0] =~ s/'Firebird'/'FirebirdEmbedded'/g;
            },
        },
    );

    copy_mangled(
        'Firebird.xs' => {
            name => 'FirebirdEmbedded.xs',
            mangle => sub {
                $_[0] =~ s/Firebird.h/FirebirdEmbedded.h/;
                $_[0] =~ s/DBD::Firebird/DBD::FirebirdEmbedded/g;
                $_[0] =~ s/(?<=^INCLUDE: Firebird).xsi/Embedded.xsi/;
            },
        },
    );

    for my $f ( glob('t/*.t') ) {
        next if $f =~ 't/embed';
        ( my $n = $f ) =~ s,t/,t/embed-,;
        copy_mangled(
            $f => {
                new_path => $n,
                mangle => sub {
                    $_[0] =~ s/DBD::Firebird\b/DBD::FirebirdEmbedded/g;
                    $_[0] =~ s/TestFirebird\b/TestFirebirdEmbedded/g;
                },
            }
        );
    }
}

1;

#-- Known registry keys

__DATA__
fb:DefaultInstance:HKEY_LOCAL_MACHINE\SOFTWARE\Firebird Project\Firebird Server\Instances
vc:ProductDir:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\6.0\Setup\Microsoft Visual C++
vc:ProductDir:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\7.0\Setup\VC
vc:ProductDir:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\9.0\Setup\VC
vc:ProductDir:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\10.0\Setup\VC
vc:ProductDir:HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\11.0\Setup\VC
pv:CurrentVersion:HKEY_LOCAL_MACHINE\SOFTWARE\ActiveState\ActivePerl
pl::HKEY_LOCAL_MACHINE\SOFTWARE\ActiveState\ActivePerl\1203
