#!/usr/bin/env perl
# App::DTWMIC - Copyright (C) 2019 Ilya Pavlov
# App::DTWMIC is licensed under the
# GNU Lesser General Public License v2.1

use strict;
use warnings;
use utf8;

use Getopt::Long;

use App::DTWMIC;
use Udev::FFI;
use YAML::Syck;

use constant {
    DEVICE_INPUT_OTHER          => 0,
    DEVICE_INPUT_MOUSE          => 1,
    DEVICE_INPUT_TOUCHPAD       => 2,
    DEVICE_INPUT_TABLET         => 3,
    DEVICE_INPUT_TOUCHSCREEN    => 4,

    SYNCLIENT_LOCATIONS => [
        '/usr/bin/synclient'
    ]
};

my $config;
my $udev;

sub _get_device_type {
    my $device = shift;

    my $id_input_mouse          = $device->get_property_value('ID_INPUT_MOUSE');
    my $id_input_touchpad       = $device->get_property_value('ID_INPUT_TOUCHPAD');
    my $id_input_tablet         = $device->get_property_value('ID_INPUT_TABLET');
    my $id_input_touchscreen    = $device->get_property_value('ID_INPUT_TOUCHSCREEN');

    if (defined($id_input_mouse) && $id_input_mouse eq '1') {
        # ID_INPUT_MOUSE: Touchscreens and tablets have this flag as well, since by the type of events they can produce
        # they act as a mouse.
        # https://askubuntu.com/questions/520359/how-to-detect-touchscreen-devices-from-a-script

        if (defined($id_input_touchpad) && $id_input_touchpad eq '1') {
            return DEVICE_INPUT_TOUCHPAD;
        }
        elsif (defined($id_input_tablet) && $id_input_tablet eq '1') {
            return DEVICE_INPUT_TABLET;
        }
        elsif (defined($id_input_touchscreen) && $id_input_touchscreen eq '1') {
            return DEVICE_INPUT_TOUCHSCREEN;
        }

        return DEVICE_INPUT_MOUSE;
    }
    elsif (defined($id_input_touchpad) && $id_input_touchpad eq '1') {
        return DEVICE_INPUT_TOUCHPAD;
    }
    elsif (defined($id_input_tablet) && $id_input_tablet eq '1') {
        return DEVICE_INPUT_TABLET;
    }
    elsif (defined($id_input_touchscreen) && $id_input_touchscreen eq '1') {
        return DEVICE_INPUT_TOUCHSCREEN;
    }

    return DEVICE_INPUT_OTHER;
}

sub _get_device_data {
    my $device = shift;

    my $parent = $device->get_parent_with_subsystem_devtype('input');

    return {}
        unless defined $parent;

    my $device_type = _get_device_type($device);

    my $name = $parent->get_property_value('NAME');
    $name = 'unknown'
        unless defined $name;

    $name =~ s/^"(.*)"$/$1/;

    return {
        type    => $device_type,
        sysname => $parent->get_sysname(),
        data    => {
            name => $name
        }
    };
}

sub _enable_touchpads($$) {
    my $toushpads = shift;
    my $is_enable = shift;

    for my $toushpad (values(%$toushpads)) {
        my $cmd = $is_enable ? $config->{'touchpadon'} : $config->{'touchpadoff'};

        if (ref($cmd) eq '') { # legacy
            $cmd =~ s/(?<=^|[^\\])\$TOUCHPAD_NAME(?=[^a-zA-Z_\d]|$)/$toushpad->{'name'}/;
            $cmd = [$cmd];
        }
        elsif (ref($cmd) eq 'ARRAY') {
            for (@$cmd) {
                s/(?<=^|[^\\])\$TOUCHPAD_NAME(?=[^a-zA-Z_\d]|$)/$toushpad->{'name'}/;
            }
        }
        else {
            return;
        }

        system(@$cmd);
    }
}

sub _update_state() {
    my $touchpads = {};
    my $has_mouse_devices = 0;

    my $enumerate = $udev->new_enumerate() or
        die("Can't create enumerate context: $@");

    $enumerate->add_match_subsystem('input') or
        die("Can't add match subsystem: $!");

    $enumerate->scan_devices() or
        die("Can't scan devices: $!");

    my $devices = $enumerate->get_list_entries() or
        die("Can't get devices: $!");

    for (keys(%$devices)) {
        if (defined(my $device = $udev->new_device_from_syspath($_))) {
            my $device_data = _get_device_data($device);
            next
                unless %$device_data;

            if (DEVICE_INPUT_TOUCHPAD == $device_data->{'type'}) {
                $touchpads->{ $device_data->{'sysname'} } = $device_data->{'data'};
            }
            elsif (DEVICE_INPUT_MOUSE == $device_data->{'type'}) {
                ++$has_mouse_devices;
            }
        }
    }

    _enable_touchpads($touchpads, 0 == $has_mouse_devices ? 1 : 0);
}

my $config_path;

{
    my $cmd_flag_help;
    my $cmd_flag_list;
    my $cmd_flag_version;

    GetOptions(
        'h|help' => \$cmd_flag_help,
        'v|version' => \$cmd_flag_version,
        'l|list' => \$cmd_flag_list,
        'f|config-file=s' => \$config_path,
    )
    or do { $cmd_flag_help = 1 };

    if (defined($cmd_flag_help)) {
        print q{Usage:
    dtwmic [OPTIONS]
    dtwmic f|config-file=CUSTOM_CONFIG_FILE_PATH

    h | help    - show help
    v | version - show dtwmic version
    l | list    - list mouse/touchpad devices

    a default config file is created on the first run in ~/.config/dtwmic/config.yml (if not exists)
};

        exit(0);
    }

    if (defined($cmd_flag_version)) {
        print("dtwmic version is $App::DTWMIC::VERSION\n");

        exit(0);
    }

    $udev = Udev::FFI->new() or
        die("Can't initialize Udev::FFI library: $@");

    if (defined($cmd_flag_list)) {
        my $enumerate = $udev->new_enumerate() or
            die("Can't create enumerate context: $@");

        $enumerate->add_match_subsystem('input') or
            die("Can't add match subsystem: $!");

        $enumerate->scan_devices() or
            die("Can't scan devices: $!");

        my $devices = $enumerate->get_list_entries() or
            die("Can't get devices: $!");

        my $input_devices = {};
        my $max_str_length = 0;
        my $str_length;

        for (keys(%$devices)) {
            if (defined(my $device = $udev->new_device_from_syspath($_))) {
                my $device_data = _get_device_data($device);
                next
                    unless %$device_data;

                $input_devices->{ $device_data->{'type'} }{ $device_data->{'sysname'} } = $device_data->{'data'};

                $str_length = length($device_data->{'data'}{'name'});
                if ($str_length > $max_str_length) {
                    $max_str_length = $str_length;
                }
            }
        }

        $max_str_length += 8 - ($max_str_length % 4);

        for my $dev_type (@{ [[DEVICE_INPUT_MOUSE, 'MOUSE DEVICES'], [DEVICE_INPUT_TOUCHPAD, 'TOUCHPAD DEVICES']] }) {
            print("$dev_type->[1]:\n");

            while (my ($sysname, $device_data) = each(%{ $input_devices->{ $dev_type->[0] } })) {
                print('  '.$device_data->{'name'}.(' ' x ($max_str_length - length($device_data->{'name'}))).
                    "sysname: $sysname\n");
            }

            print("\n");
        }

        exit(0);
    }
}

if (!defined($config_path)) {
    require File::HomeDir;

    my $config_dir = File::HomeDir->my_home().'/.config/dtwmic/';
    unless (-d $config_dir) {
        require File::Path;
        File::Path->import('make_path');

        make_path($config_dir, {error => \my $err});
        die("Can't create directory '$config_dir': ".$err->[0]."\n")
            if (@$err);
    }

    $config_path = $config_dir.'config.yml';

    unless (-f $config_path) {
        require File::Which;
        File::Which->import();

        my $synclient_path = which('synclient');
        if(!defined($synclient_path)) {
            for(@{ SYNCLIENT_LOCATIONS() }) {
                if(-f) {
                    $synclient_path = $_;

                    last;
                }
            }
        }

        my $fh;
        open($fh, '>', $config_path) or
            die("Can't create default config file $config_path: $!\n");

        if (defined($synclient_path)) {
            $synclient_path =~ s/"/\\"/;

            print $fh qq{# \$TOUCHPAD_NAME variable available here
touchpadon: ["$synclient_path", touchpadoff=0]
touchpadoff: ["$synclient_path", touchpadoff=1]
};
        }
        else {
            print $fh qq{# \$TOUCHPAD_NAME variable available here
touchpadon: [xinput, set-prop, \$TOUCHPAD_NAME, "Device Enabled", 1]
touchpadoff: [xinput, set-prop, \$TOUCHPAD_NAME, "Device Enabled", 0]
};
        }

        close($fh);
    }
}

eval {
    $config = YAML::Syck::LoadFile($config_path);
};
if ($@) {
    die("Can't load config file $config_path: $@\n");
}

my $monitor = $udev->new_monitor() or
    die "Can't create udev monitor: $@";

$monitor->filter_by_subsystem_devtype('input') or
    die "Can't add filter to udev monitor: $!";

$monitor->start() or
    die "Can't start udev monitor: $!";

_update_state();

for (;;) {
    my $device = $monitor->poll();
    my $action = $device->get_action();

    if ($action eq 'add' || $action eq 'remove') {
        _update_state();
    }
}



__END__

=encoding utf-8

=head1 NAME

dtwmic - disable touchpad when a mouse is connected

=head1 INSTALL

    curl -L https://cpanmin.us | perl - App::DTWMIC

Note: you may wish to install a package like libffi-platypus-perl on your distro
to fast installation

=head1 SYNOPSIS

    dtwmic [OPTIONS]
    dtwmic f | config-file=CUSTOM_CONFIG_FILE_PATH
    
    h | help    - show help
    v | version - show dtwmic version
    l | list    - list mouse/touchpad devices

=head1 DESCRIPTION

Add dtwmic to your X window manager autostart.

A default configuration file is created on the first run in
~/.config/dtwmic/config.yml (if not exists).

You need synclient or xinput by default otherwise edit the config.

=head1 BUGS

Please report any bugs through the web interface at
L<https://github.com/Ilya33/App-DTWMIC/issues>. Patches are always welcome.

=head1 AUTHOR

Ilya Pavlov E<lt>ilux@cpan.orgE<gt>

=head1 COPYRIGHT

Copyright 2019- Ilya Pavlov

=head1 LICENSE

GNU Lesser General Public License v2.1

=cut
