use strict;
use warnings;
use lib '../lib', '../blib/arch', '../blib/lib', 'blib/arch', 'blib/lib';
use Affix     qw[:all];
use Dyn::Load qw[:all];
use Dyn::Call qw[/dc/];
use FFI::Platypus 1.58;
use Config;
use Benchmark qw[cmpthese timethese :hireswallclock];

# arbitrary benchmarks
$|++;
our $libfile
    = $^O eq 'MSWin32' ? 'ntdll.dll' :
    $^O eq 'darwin'    ? '/usr/lib/libm.dylib' :
    $^O eq 'bsd'       ? '/usr/lib/libm.so' :
    $Config{archname} =~ /64/ ?
    -e '/lib64/libm.so.6' ?
    '/lib64/libm.so.6' :
        '/lib/x86_64-linux-gnu/libm.so.6' :
    '/lib/libm.so.6';

sub libfile {
    $libfile;
}

package FFI {
    use strictures 2;
    use feature 'signatures';
    use Config;
    use File::Spec;
    use File::Basename qw[dirname];
    use File::Find     qw[find];
    use Dyn            qw[:all];
    my $cvm = dcNewCallVM(1024);
    dcMode( $cvm, DC_CALL_C_DEFAULT );
    END { dcFree($cvm) if $cvm }

    sub obj ( $package, $ptr, $signature ) {
        my @funcs;
        my $ret = \&dcCallVoid;
        CORE::state $arg_dispatch //= {
            DC_SIGCHAR_BOOL()     => \&dcArgBool,
            DC_SIGCHAR_CHAR()     => \&dcArgChar,
            DC_SIGCHAR_SHORT()    => \&dcArgShort,
            DC_SIGCHAR_INT()      => \&dcArgInt,
            DC_SIGCHAR_LONG()     => \&dcArgLong,
            DC_SIGCHAR_LONGLONG() => \&dcArgLongLong,
            DC_SIGCHAR_FLOAT()    => \&dcArgFloat,
            DC_SIGCHAR_DOUBLE()   => \&dcArgDouble,
            DC_SIGCHAR_POINTER()  => \&dcArgPointer,
            DC_SIGCHAR_STRING()   => \&dcArgString
        };
        CORE::state $ret_dispatch //= {
            DC_SIGCHAR_VOID()     => \&dcCallVoid,
            DC_SIGCHAR_BOOL()     => \&dcCallBool,
            DC_SIGCHAR_CHAR()     => \&dcCallChar,
            DC_SIGCHAR_SHORT()    => \&dcCallShort,
            DC_SIGCHAR_INT()      => \&dcCallInt,
            DC_SIGCHAR_LONG()     => \&dcCallLong,
            DC_SIGCHAR_LONGLONG() => \&dcCallLongLong,
            DC_SIGCHAR_FLOAT()    => \&dcCallFloat,
            DC_SIGCHAR_DOUBLE()   => \&dcCallDouble,
            DC_SIGCHAR_POINTER()  => \&dcCallPointer,
            DC_SIGCHAR_STRING()   => \&dcCallString
        };
        my @chars = split //, $signature;
        while ( my $sigchar = shift @chars ) {
            if ( $sigchar eq ')' ) {
                $ret = $ret_dispatch->{ shift @chars };
                last;
            }
            push @funcs, $arg_dispatch->{$sigchar};
        }
        my $thing = sub {
            dcReset($cvm);
            $funcs[$_]->( $cvm, $_[$_] ) for 0 .. $#funcs;
            $ret->( $cvm, $ptr );
        };
        bless $thing, $package;
    }

    sub load ( $path, $version = () ) {
        dlLoadLibrary( guess_library_name( $path, $version ) );
    }
    sub func ( $lib, $symbol ) { dlFindSymbol( $lib, $symbol ); }
    our $OS = $^O;

    sub guess_library_name ( $name = (), $version = () ) {
        ( $name, $version ) = @$name if ref $name eq 'ARRAY';
        $name // return ();    # NULL
        return $name if -f $name;
        CORE::state $_lib_cache;
        my @retval;
        ($version) = version->parse($version)->stringify =~ m[^v?(.+)$];

        # warn $version;
        $version = $version ? qr[\.${version}] : qr/([\.\d]*)?/;
        if ( !defined $_lib_cache->{ $name . chr(0) . ( $version // '' ) } ) {
            if ( $OS eq 'MSWin32' ) {
                $name =~ s[\.dll$][];

                #return $name . '.dll'     if -f $name . '.dll';
                return File::Spec->canonpath( File::Spec->rel2abs( $name . '.dll' ) )
                    if -e $name . '.dll';
                require Win32;

# https://docs.microsoft.com/en-us/windows/win32/dlls/dynamic-link-library-search-order#search-order-for-desktop-applications
                my @dirs = grep {-d} (
                    dirname( File::Spec->rel2abs($^X) ),               # 1. exe dir
                    Win32::GetFolderPath( Win32::CSIDL_SYSTEM() ),     # 2. sys dir
                    Win32::GetFolderPath( Win32::CSIDL_WINDOWS() ),    # 4. win dir
                    File::Spec->rel2abs( File::Spec->curdir ),         # 5. cwd
                    File::Spec->path(),                                # 6. $ENV{PATH}

                    #'C:\Program Files'
                );
                warn $_ for sort { lc $a cmp lc $b } @dirs;
                find(
                    {   wanted => sub {
                            $File::Find::prune = 1
                                if !grep { $_ eq $File::Find::name } @dirs;    # no depth
                            push @retval, $_ if m{[/\\]${name}(-${version})?\.dll$}i;
                        },
                        no_chdir => 1
                    },
                    @dirs
                );
            }
            elsif ( $OS eq 'darwin' ) {
                return $name . '.so'     if -f $name . '.so';
                return $name . '.dylib'  if -f $name . '.dylib';
                return $name . '.bundle' if -f $name . '.bundle';
                return $name             if $name =~ /\.so$/;
                return $name;    # Let 'em work it out

# https://developer.apple.com/library/archive/documentation/DeveloperTools/Conceptual/DynamicLibraries/100-Articles/UsingDynamicLibraries.html
                my @dirs = (
                    dirname( File::Spec->rel2abs($^X) ),          # 0. exe dir
                    File::Spec->rel2abs( File::Spec->curdir ),    # 0. cwd
                    File::Spec->path(),                           # 0. $ENV{PATH}
                    map      { File::Spec->rel2abs($_) }
                        grep { -d $_ } qw[~/lib /usr/local/lib /usr/lib],
                    map { $ENV{$_} // () }
                        qw[LD_LIBRARY_PATH DYLD_LIBRARY_PATH DYLD_FALLBACK_LIBRARY_PATH]
                );

                #use Test::More;
                #diag join ', ', @dirs;
                #warn;
                find(
                    {   wanted => sub {
                            $File::Find::prune = 1
                                if !grep { $_ eq $File::Find::name } @dirs;    # no depth
                            push @retval, $_ if /\b(?:lib)?${name}${version}\.(so|bundle|dylib)$/;
                        },
                        no_chdir => 1
                    },
                    @dirs
                );

                #diag join ', ', @retval;
            }
            else {
                return $name . '.so' if -f $name . '.so';
                return $name         if -f $name;
                my $ext = $Config{so};
                my @libs;

               # warn $name . '.' . $ext . $version;
               #\b(?:lib)?${name}(?:-[\d\.]+)?\.${ext}${version}
               #my @lines = map { [/^\t(.+)\s\((.+)\)\s+=>\s+(.+)$/] }
               #    grep {/\b(?:lib)?${name}(?:-[\d\.]+)?\.${ext}(?:\.${version})?$/} `ldconfig -p`;
               #push @retval, map { $_->[2] } grep { -f $_->[2] } @lines;
                my @dirs = (
                    dirname( File::Spec->rel2abs($^X) ),          # 0. exe dir
                    File::Spec->rel2abs( File::Spec->curdir ),    # 0. cwd
                    File::Spec->path(),                           # 0. $ENV{PATH}
                    map      { File::Spec->rel2abs($_) }
                        grep { -d $_ } qw[~/lib /usr/local/lib /usr/lib],
                    map { $ENV{$_} // () }
                        qw[LD_LIBRARY_PATH DYLD_LIBRARY_PATH DYLD_FALLBACK_LIBRARY_PATH]
                );

                #use Data::Dump;
                #ddx \@dirs;
                find(
                    {   wanted => sub {
                            $File::Find::prune = 1
                                if !grep { $_ eq $File::Find::name } @dirs;    # no depth
                            push @retval, $_ if /\b(?:lib)?${name}(?:-[\d\.]+)?\.${ext}${version}$/;
                            push @retval, $_ if /\b(?:lib)?${name}(?:-[\d\.]+)?\.${ext}$/;
                        },
                        no_chdir => 1
                    },
                    @dirs
                );
            }
            $_lib_cache->{ $name . chr(0) . ( $version // '' ) } = pop @retval;
        }

        # TODO: Make a test with a bad lib name
        $_lib_cache->{ $name . chr(0) . ( $version // '' ) }
            // Carp::croak( 'Cannot locate symbol: ' . $name );
    }
}
my $hand_rolled = FFI->obj( FFI::func( FFI::load($libfile), 'sin' ), 'd)d' );
#
sub sin_ : Native(libfile) : Signature([Double]=>Double) : Symbol('sin');
sub sin_var : Native(libfile) : Signature([CC_ELLIPSIS_VARARGS,Double]=>Double) : Symbol('sin');
sub sin_ell : Native(libfile) : Signature([CC_ELLIPSIS,Double]=>Double) : Symbol('sin');
sub sin_cdecl : Native(libfile) : Signature([CC_CDECL,Double]=>Double) : Symbol('sin');
sub sin_std : Native(libfile) : Signature([CC_STDCALL,Double]=>Double) : Symbol('sin');
sub sin_fc : Native(libfile) : Signature([CC_FASTCALL_GNU,Double]=>Double) : Symbol('sin');
sub sin_tc : Native(libfile) : Signature([CC_THISCALL_GNU,Double]=>Double) : Symbol('sin');
#
my $sin_default  = wrap( $libfile, 'sin', [Double] => Double );
my $sin_vararg   = wrap( $libfile, 'sin', [ CC_ELLIPSIS_VARARGS, Double ] => Double );
my $sin_ellipsis = wrap( $libfile, 'sin', [ CC_ELLIPSIS,         Double ] => Double );
my $sin_cdecl    = wrap( $libfile, 'sin', [ CC_CDECL,            Double ] => Double );
my $sin_stdcall  = wrap( $libfile, 'sin', [ CC_STDCALL,          Double ] => Double );
my $sin_fastcall = wrap( $libfile, 'sin', [ CC_FASTCALL_GNU,     Double ] => Double );
my $sin_thiscall = wrap( $libfile, 'sin', [ CC_THISCALL_GNU,     Double ] => Double );
#
affix( $libfile, 'sin', [Double] => Double, '_affix_sin_default' );
affix( $libfile, 'sin', [ CC_ELLIPSIS_VARARGS, Double ] => Double, '_affix_sin_var' );
affix( $libfile, 'sin', [ CC_ELLIPSIS,         Double ] => Double, '_affix_sin_ellipse' );
affix( $libfile, 'sin', [ CC_CDECL,            Double ] => Double, '_affix_sin_cdecl' );
affix( $libfile, 'sin', [ CC_STDCALL,          Double ] => Double, '_affix_sin_std' );
affix( $libfile, 'sin', [ CC_FASTCALL_GNU,     Double ] => Double, '_affix_sin_fc' );
affix( $libfile, 'sin', [ CC_THISCALL_GNU,     Double ] => Double, '_affix_sin_tc' );
#
my $ffi = FFI::Platypus->new( api => 1 );
$ffi->lib($libfile);
my $ffi_func = $ffi->function( sin => ['double'] => 'double' );
$ffi->attach( [ sin => 'ffi_sin' ] => ['double'] => 'double' );

# prime the pump
my $sin = sin 500;
{
    die 'oops' if $hand_rolled->(500) != $sin;
    die 'oops' if sin_(500) != $sin;
    die 'oops' if sin_var(500) != $sin;
    die 'oops' if sin_ell(500) != $sin;
    die 'oops' if sin_std(500) != $sin;
    die 'oops' if sin_fc(500) != $sin;
    die 'oops' if sin_tc(500) != $sin;
    die 'oops' if $sin_default->(500) != $sin;
    die 'oops' if $sin_vararg->(500) != $sin;
    die 'oops' if $sin_ellipsis->(500) != $sin;
    die 'oops' if $sin_cdecl->(500) != $sin;
    die 'oops' if $sin_stdcall->(500) != $sin;
    die 'oops' if $sin_fastcall->(500) != $sin;
    die 'oops' if $sin_thiscall->(500) != $sin;
    die 'oops' if _affix_sin_default(500) != $sin;
    die 'oops' if _affix_sin_var(500) != $sin;
    die 'oops' if _affix_sin_ellipse(500) != $sin;
    die 'oops' if _affix_sin_cdecl(500) != $sin;
    die 'oops' if _affix_sin_std(500) != $sin;
    die 'oops' if _affix_sin_fc(500) != $sin;
    die 'oops' if _affix_sin_tc(500) != $sin;
    die 'oops' if ffi_sin(500) != $sin;
    die 'oops' if $ffi_func->(500) != $sin;
    die 'oops' if $ffi_func->call(500) != $sin;
}
#
my $depth = 1000;
cmpthese(
    timethese(
        -10,
        {   hand_rolled => sub {
                my $x = 0;
                while ( $x < $depth ) { my $n = $hand_rolled->($x); $x++ }
            },
            perl => sub {
                my $x = 0;
                while ( $x < $depth ) { my $n = sin($x); $x++ }
            },
            affix_sin_default => sub {
                my $x = 0;
                while ( $x < $depth ) { my $n = _affix_sin_default($x); $x++ }
            },
            affix_sin_var => sub {
                my $x = 0;
                while ( $x < $depth ) { my $n = _affix_sin_var($x); $x++ }
            },
            affix_sin_ellipse => sub {
                my $x = 0;
                while ( $x < $depth ) { my $n = _affix_sin_ellipse($x); $x++ }
            },
            affix_sin_cdecl => sub {
                my $x = 0;
                while ( $x < $depth ) { my $n = _affix_sin_cdecl($x); $x++ }
            },
            affix_sin_std => sub {
                my $x = 0;
                while ( $x < $depth ) { my $n = _affix_sin_std($x); $x++ }
            },
            affix_sin_fc => sub {
                my $x = 0;
                while ( $x < $depth ) { my $n = _affix_sin_fc($x); $x++ }
            },
            affix_sin_tc => sub {
                my $x = 0;
                while ( $x < $depth ) { my $n = _affix_sin_tc($x); $x++ }
            },
            sub_sin_default => sub {
                my $x = 0;
                while ( $x < $depth ) { my $n = sin_($x); $x++ }
            },
            sub_sin_var => sub {
                my $x = 0;
                while ( $x < $depth ) { my $n = sin_var($x); $x++ }
            },
            sub_sin_ell => sub {
                my $x = 0;
                while ( $x < $depth ) { my $n = sin_ell($x); $x++ }
            },
            sub_sin_cdecl => sub {
                my $x = 0;
                while ( $x < $depth ) { my $n = sin_cdecl($x); $x++ }
            },
            sub_sin_std => sub {
                my $x = 0;
                while ( $x < $depth ) { my $n = sin_std($x); $x++ }
            },
            sub_sin_fc => sub {
                my $x = 0;
                while ( $x < $depth ) { my $n = sin_fc($x); $x++ }
            },
            sub_sin_tc => sub {
                my $x = 0;
                while ( $x < $depth ) { my $n = sin_tc($x); $x++ }
            },
            call_default => sub {
                my $x = 0;
                while ( $x < $depth ) { my $n = $sin_default->($x); $x++ }
            },
            call_vararg => sub {
                my $x = 0;
                while ( $x < $depth ) { my $n = $sin_vararg->($x); $x++ }
            },
            call_ellipsis => sub {
                my $x = 0;
                while ( $x < $depth ) { my $n = $sin_ellipsis->($x); $x++ }
            },
            call_cdecl => sub {
                my $x = 0;
                while ( $x < $depth ) { my $n = $sin_cdecl->($x); $x++ }
            },
            call_stdcall => sub {
                my $x = 0;
                while ( $x < $depth ) { my $n = $sin_stdcall->($x); $x++ }
            },
            call_fastcall => sub {
                my $x = 0;
                while ( $x < $depth ) { my $n = $sin_fastcall->($x); $x++ }
            },
            call_thiscall => sub {
                my $x = 0;
                while ( $x < $depth ) { my $n = $sin_thiscall->($x); $x++ }
            },
            ffi_attach => sub {
                my $x = 0;
                while ( $x < $depth ) { my $n = ffi_sin($x); $x++ }
            },
            ffi_function => sub {
                my $x = 0;
                while ( $x < $depth ) { my $n = $ffi_func->($x); $x++ }
            },
            ffi_function_call => sub {
                my $x = 0;
                while ( $x < $depth ) { my $n = $ffi_func->call($x); $x++ }
            }
        }
    )
);
