package Udev::FFI::Functions;

use strict;
use warnings;

use base qw(Exporter);

use File::Which ();
use FFI::CheckLib;
use FFI::Platypus;

use constant {
    UDEVADM_LOCATIONS => [
        '/bin/udevadm',
        '/sbin/udevadm'
    ]
};

my $FUNCTIONS = {
    # struct udev *udev_new(void);
    'udev_new' => {
        ffi_data => [ [], 'opaque' ]
    },

    # struct udev *udev_ref(struct udev *udev);
    'udev_ref' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    },

    # struct udev *udev_unref(struct udev *udev);
    'udev_unref' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    },

    # access to libudev generated lists ====================================

    # struct udev_list_entry *udev_list_entry_get_next(struct udev_list_entry *list_entry);
    'udev_list_entry_get_next' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    },

    # struct udev_list_entry *udev_list_entry_get_by_name(struct udev_list_entry *list_entry, const char *name);
    'udev_list_entry_get_by_name' => {
        ffi_data => [ ['opaque', 'string'], 'opaque']
    },

    # const char *udev_list_entry_get_name(struct udev_list_entry *list_entry);
    'udev_list_entry_get_name' => {
        ffi_data => [ ['opaque'], 'string' ]
    },

    # const char *udev_list_entry_get_value(struct udev_list_entry *list_entry);
    'udev_list_entry_get_value' => {
        ffi_data => [ ['opaque'], 'string' ]
    },


    # udev_device ==========================================================

    # struct udev_device *udev_device_ref(struct udev_device *udev_device);
    'udev_device_ref' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    },

    # struct udev_device *udev_device_unref(struct udev_device *udev_device);
    'udev_device_unref' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    },

    # struct udev *udev_device_get_udev(struct udev_device *udev_device);
    'udev_device_get_udev' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    },

    # struct udev_device *udev_device_new_from_syspath(struct udev *udev, const char *syspath);
    'udev_device_new_from_syspath' => {
        ffi_data => [ ['opaque', 'string'], 'opaque' ]
    },

    # struct udev_device *udev_device_new_from_devnum(struct udev *udev, char type, dev_t devnum);
    'udev_device_new_from_devnum' => {
        ffi_data => [ ['opaque', 'signed char', 'dev_t'], 'opaque' ]
    },

    # struct udev_device *udev_device_new_from_subsystem_sysname(struct udev *udev, const char *subsystem, const char *sysname);
    'udev_device_new_from_subsystem_sysname' => {
        ffi_data => [ ['opaque', 'string', 'string'], 'opaque' ]
    },

    # struct udev_device *udev_device_new_from_device_id(struct udev *udev, const char *id);
    'udev_device_new_from_device_id' => {
        ffi_data => [ ['opaque', 'string'], 'opaque' ],
        since    => 189,
    },

    # struct udev_device *udev_device_new_from_environment(struct udev *udev);
    'udev_device_new_from_environment' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    },

    # struct udev_device *udev_device_get_parent(struct udev_device *udev_device);
    'udev_device_get_parent' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    },

    # struct udev_device *udev_device_get_parent_with_subsystem_devtype(struct udev_device *udev_device,
    #     const char *subsystem, const char *devtype);
    'udev_device_get_parent_with_subsystem_devtype' => {
        ffi_data => [ ['opaque', 'string', 'string'], 'opaque' ]
    },

    # retrieve device properties

    # const char *udev_device_get_devpath(struct udev_device *udev_device);
    'udev_device_get_devpath'  => {
        ffi_data => [ ['opaque'], 'string' ]
    },

    # const char *udev_device_get_subsystem(struct udev_device *udev_device);
    'udev_device_get_subsystem' => {
        ffi_data => [ ['opaque'], 'string' ]
    },

    # const char *udev_device_get_devtype(struct udev_device *udev_device);
    'udev_device_get_devtype' => {
        ffi_data => [ ['opaque'], 'string' ]
    },

    # const char *udev_device_get_syspath(struct udev_device *udev_device);
    'udev_device_get_syspath' => {
        ffi_data => [ ['opaque'], 'string' ]
    },

    # const char *udev_device_get_sysname(struct udev_device *udev_device);
    'udev_device_get_sysname' => {
        ffi_data => [ ['opaque'], 'string' ]
    },

    # const char *udev_device_get_sysnum(struct udev_device *udev_device);
    'udev_device_get_sysnum' => {
        ffi_data => [ ['opaque'], 'string' ]
    },

    # const char *udev_device_get_devnode(struct udev_device *udev_device);
    'udev_device_get_devnode' => {
        ffi_data => [ ['opaque'], 'string' ]
    },

    #int udev_device_get_is_initialized(struct udev_device *udev_device);
    'udev_device_get_is_initialized' => {
        ffi_data => [ ['opaque'], 'int' ]
    },

    # struct udev_list_entry *udev_device_get_devlinks_list_entry(struct udev_device *udev_device);
    'udev_device_get_devlinks_list_entry' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    },

    # struct udev_list_entry *udev_device_get_properties_list_entry(struct udev_device *udev_device);
    'udev_device_get_properties_list_entry' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    },

    # struct udev_list_entry *udev_device_get_tags_list_entry(struct udev_device *udev_device);
    'udev_device_get_tags_list_entry' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    },

    # struct udev_list_entry *udev_device_get_sysattr_list_entry(struct udev_device *udev_device);
    'udev_device_get_sysattr_list_entry' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    },

    #const char *udev_device_get_property_value(struct udev_device *udev_device, const char *key);
    'udev_device_get_property_value' => {
        ffi_data => [ ['opaque', 'string'], 'string' ]
    },

    #const char *udev_device_get_driver(struct udev_device *udev_device);
    'udev_device_get_driver' => {
        ffi_data => [ ['opaque'], 'string' ]
    },

    # dev_t udev_device_get_devnum(struct udev_device *udev_device);
    'udev_device_get_devnum' => {
        ffi_data => [ ['opaque'], 'dev_t' ]
    },

    #const char *udev_device_get_action(struct udev_device *udev_device);
    'udev_device_get_action' => {
        ffi_data => [ ['opaque'], 'string' ]
    },

    #unsigned long long int udev_device_get_seqnum(struct udev_device *udev_device);
    'udev_device_get_seqnum' => {
        ffi_data => [ ['opaque'], 'unsigned long long' ]
    },

    #unsigned long long int udev_device_get_usec_since_initialized(struct udev_device *udev_device);
    'udev_device_get_usec_since_initialized' => {
        ffi_data => [ ['opaque'], 'unsigned long long' ]
    },

    #const char *udev_device_get_sysattr_value(struct udev_device *udev_device, const char *sysattr);
    'udev_device_get_sysattr_value' => {
        ffi_data => [ ['opaque', 'string'], 'string' ]
    },

    #int udev_device_set_sysattr_value(struct udev_device *udev_device, const char *sysattr, char *value);
    'udev_device_set_sysattr_value' => {
        ffi_data => [ ['opaque', 'string', 'string'], 'int' ],
        since    => 199,
    },

    #int udev_device_has_tag(struct udev_device *udev_device, const char *tag);
    'udev_device_has_tag' => {
        ffi_data => [ ['opaque', 'string'], 'int' ],
        since    => 172,
    },


    # udev_monitor =========================================================

    # access to kernel uevents and udev events

    # struct udev_monitor *udev_monitor_ref(struct udev_monitor *udev_monitor);
    'udev_monitor_ref' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    },

    # struct udev_monitor *udev_monitor_unref(struct udev_monitor *udev_monitor);
    'udev_monitor_unref' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    },

    # struct udev *udev_monitor_get_udev(struct udev_monitor *udev_monitor);
    'udev_monitor_get_udev' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    },

    #kernel and udev generated events over netlink

    # struct udev_monitor *udev_monitor_new_from_netlink(struct udev *udev, const char *name);
    'udev_monitor_new_from_netlink' => {
        ffi_data => [ ['opaque', 'string'], 'opaque' ]
    },

    # bind socket

    # int udev_monitor_enable_receiving(struct udev_monitor *udev_monitor);
    'udev_monitor_enable_receiving' => {
        ffi_data => [ ['opaque'], 'int' ]
    },

    # int udev_monitor_set_receive_buffer_size(struct udev_monitor *udev_monitor, int size);
    'udev_monitor_set_receive_buffer_size' => {
        ffi_data => [ ['opaque', 'int'], 'int' ]
    },

    # int udev_monitor_get_fd(struct udev_monitor *udev_monitor);
    'udev_monitor_get_fd' => {
        ffi_data => [ ['opaque'], 'int' ]
    },

    # struct udev_device *udev_monitor_receive_device(struct udev_monitor *udev_monitor);
    'udev_monitor_receive_device' => {
        ffi_data => [ ['opaque'], 'opaque']
    },

    # n-kernel socket filters to select messages that get delivered to a listener

    # int udev_monitor_filter_add_match_subsystem_devtype(struct udev_monitor *udev_monitor,
    #     const char *subsystem, const char *devtype);
    'udev_monitor_filter_add_match_subsystem_devtype' => {
        ffi_data => [ ['opaque', 'string', 'string'], 'int' ]
    },

    # int udev_monitor_filter_add_match_tag(struct udev_monitor *udev_monitor, const char *tag);
    'udev_monitor_filter_add_match_tag' => {
        ffi_data => [ ['opaque', 'string'], 'int' ]
    },

    # int udev_monitor_filter_update(struct udev_monitor *udev_monitor);
    'udev_monitor_filter_update' => {
        ffi_data => [ ['opaque'], 'int' ]
    },

    # int udev_monitor_filter_remove(struct udev_monitor *udev_monitor);
    'udev_monitor_filter_remove' => {
        ffi_data => [ ['opaque'], 'int' ]
    },


    # udev_enumerate =======================================================

    # search sysfs for specific devices and provide a sorted list

    # struct udev_enumerate *udev_enumerate_ref(struct udev_enumerate *udev_enumerate);
    'udev_enumerate_ref' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    },

    # struct udev_enumerate *udev_enumerate_unref(struct udev_enumerate *udev_enumerate);
    'udev_enumerate_unref' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    },

    # struct udev *udev_enumerate_get_udev(struct udev_enumerate *udev_enumerate);
    'udev_enumerate_get_udev' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    },

    # struct udev_enumerate *udev_enumerate_new(struct udev *udev);
    'udev_enumerate_new' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    },

    # device properties filter

    # int udev_enumerate_add_match_subsystem(struct udev_enumerate *udev_enumerate, const char *subsystem);
    'udev_enumerate_add_match_subsystem' => {
        ffi_data => [ ['opaque', 'string'], 'int' ]
    },

    # int udev_enumerate_add_nomatch_subsystem(struct udev_enumerate *udev_enumerate, const char *subsystem);
    'udev_enumerate_add_nomatch_subsystem' => {
        ffi_data => [ ['opaque', 'string'], 'int' ]
    },

    # int udev_enumerate_add_match_sysattr(struct udev_enumerate *udev_enumerate, const char *sysattr, const char *value);
    'udev_enumerate_add_match_sysattr' => {
        ffi_data => [ ['opaque', 'string', 'string'], 'int' ]
    },

    # int udev_enumerate_add_nomatch_sysattr(struct udev_enumerate *udev_enumerate, const char *sysattr, const char *value);
    'udev_enumerate_add_nomatch_sysattr' => {
        ffi_data => [ ['opaque', 'string', 'string'], 'int' ]
    },

    # int udev_enumerate_add_match_property(struct udev_enumerate *udev_enumerate, const char *property, const char *value);
    'udev_enumerate_add_match_property' => {
        ffi_data => [ ['opaque', 'string', 'string'], 'int' ]
    },

    # int udev_enumerate_add_match_sysname(struct udev_enumerate *udev_enumerate, const char *sysname);
    'udev_enumerate_add_match_sysname' => {
        ffi_data => [ ['opaque', 'string'], 'int' ]
    },

    # int udev_enumerate_add_match_tag(struct udev_enumerate *udev_enumerate, const char *tag);
    'udev_enumerate_add_match_tag' => {
        ffi_data => [ ['opaque', 'string'], 'int' ]
    },

    # int udev_enumerate_add_match_parent(struct udev_enumerate *udev_enumerate, struct udev_device *parent);
    'udev_enumerate_add_match_parent' => {
        ffi_data => [ ['opaque', 'opaque'], 'int' ],
        since    => 172,
    },

    # int udev_enumerate_add_match_is_initialized(struct udev_enumerate *udev_enumerate);
    'udev_enumerate_add_match_is_initialized' => {
        ffi_data => [ ['opaque'], 'int' ]
    },

    # int udev_enumerate_add_syspath(struct udev_enumerate *udev_enumerate, const char *syspath);
    'udev_enumerate_add_syspath' => {
        ffi_data => [ ['opaque', 'string'], 'int' ]
    },

    # run enumeration with active filters

    # int udev_enumerate_scan_devices(struct udev_enumerate *udev_enumerate);
    'udev_enumerate_scan_devices' => {
        ffi_data => [ ['opaque'], 'int' ]
    },

    # int udev_enumerate_scan_subsystems(struct udev_enumerate *udev_enumerate);
    'udev_enumerate_scan_subsystems' => {
        ffi_data => [ ['opaque'], 'int' ]
    },

    # return device list

    # struct udev_list_entry *udev_enumerate_get_list_entry(struct udev_enumerate *udev_enumerate);
    'udev_enumerate_get_list_entry' => {
        ffi_data => [ ['opaque'], 'opaque' ]
    }
};



our @EXPORT_OK = keys(%$FUNCTIONS);

our %EXPORT_TAGS = (
    'all' => \@EXPORT_OK
);


my $init = 0;



sub udev_version {
    my $full_path = File::Which::which('udevadm');

    unless (defined($full_path)) {
        for (@{ UDEVADM_LOCATIONS() }) {
            if (-f) {
                $full_path = $_;

                last;
            }
        }
    }

    unless (defined($full_path)) {
        $@ = "Can't find `udevadm` utility";

        return undef;
    }

    {
        local $SIG{__WARN__} = sub {}; # silence shell output if error

        # TODO timeout
        if (open(my $ph, '-|', $full_path, '--version')) {
            my $out = <$ph>;

            if (defined($out) && $out =~ /^(\d+)\s*$/) {
                return $1;
            }

            $@ = "Can't get udev version from `udevadm` utility";

            return undef;
        }
    }

    $@ = "Can't run `udevadm` utility";

    return undef;
}



my $_function_not_attach = sub {
    my $udev_version = udev_version();

    die("function '".$_[0]."' not attached from udev library\n`udevadm` ".
        "version: ".(defined($udev_version) ? $udev_version : 'unknown')."\n");
};



sub init {
    return 1
        if 1 == $init;

    my ($libudev) = find_lib(lib => 'udev');
    unless (defined($libudev) || $libudev eq '') {
        $@ = "Can't find udev library";
        return 0;
    }

    my $udev_version = udev_version();
    $udev_version = 0
        unless defined($udev_version);

    my $ffi = FFI::Platypus->new();
    $ffi->lib($libudev);

    unless ($ffi->type('dev_t')) {
        $@ = "Can't find \"dev_t\" type";
        return 0;
    }

    for my $funct (keys(%$FUNCTIONS)) {
        eval {
            # attach locks the function and the FFI::Platypus instance into memory permanently,
            # since there is no way to deallocate an xsub
            $ffi->attach($funct => $FUNCTIONS->{$funct}{ffi_data}[0] => $FUNCTIONS->{$funct}{ffi_data}[1]);
        };
        if ($@) {
            if (!exists($FUNCTIONS->{$funct}{since}) || $udev_version >= $FUNCTIONS->{$funct}{since}) {
                $@ = $1
                    if $@ =~ m{^(.*)\s+at\s.*line\s\d+.}xms;

                $@ = "Can't attach '$funct' function from udev library: $@";
                return 0;
            }

            no strict 'refs';
            *$funct = sub { $_function_not_attach->($funct) };
        }

        # function attached
        delete($FUNCTIONS->{$funct});
    }

    return ++$init;
}



1;
