#!/usr/bin/perl
#
# usage: pm-bluetooth [--debug] {suspend|resume}
#

use 5.020;
use strict;
use warnings;

use FindBin;
use Getopt::Long qw(:config bundling);
use JSON;

my $PROG        = $FindBin::Script;
my $STATUS_FILE = "/run/$PROG.state";

my $debug    = 0;
my $verbose  = 0;
my $dry_run  = 0;

sub verbose(@);
sub debug(@);
sub fatal(@);

my $USAGE=<<EOF;
usage: $PROG [--debug] [--verbose] [--dry-run] {suspend|resume}
       $PROG [-d] [-v] [-n] {suspend|resume}
EOF

sub Main {
    GetOptions(
        'dry-run|n' => \$dry_run,
        'debug|d'   => \$debug,
        'verbose|v' => \$verbose,
        'help|h|?'  => sub { die $USAGE },
    ) || die $USAGE;

    @ARGV == 1 or die $USAGE;

    $debug += $dry_run;
    $verbose += $debug;

    if ($ARGV[0] eq 'suspend') {
        verbose "$PROG: suspend";
        my $device_list = save_state($STATUS_FILE);
        power_bluetooth('off', $device_list);
        sleep(2);
        exit 0;
    }

    if ($ARGV[0] eq 'resume') {
        verbose "$PROG: resume";
        restore_state($STATUS_FILE);
        exit 0;
    }

    die $USAGE;
}


################################################

sub fatal(@)    {
    chomp(my $str = join('', @_));
    $str .= "\nbailing out";
    $str =~ s/^/$PROG: /gm;
    say STDERR $str;
    exit 1;
}

sub verbose(@)  { say STDOUT @_ if $verbose }

sub debug(@)    {
    return unless $debug;
    chomp(my $str = join('', @_));
    $str =~ s/^/#DEBUG: /gm;
    say STDERR $str;
}

sub get_bluetooth_status {
    verbose "fetching Bluetooth status";
    open my $fh, '-|', 'bluetoothctl', 'show'
        or fatal "cannot read from bluetoothctl pipe: $!";
    
    my $bt_status = do { local($/) = undef; <$fh> };
    close $fh;

    my @device_list;
    while (
        $bt_status =~ m{^
            Controller \s (?<device>(?:[a-f\d]{2}:){5}[a-f\d]{2})
            .*?\n
            \s+ Powered: \s (?<powered>\S+)
        }gimsx
    ) {
        push @device_list, {
            device => $+{device},
            powered => $+{powered} eq 'yes' ? \1 : \0,
        };
    }

    debug("Bluetooth status:\n", JSON->new->pretty->encode(\@device_list));
    return \@device_list;
}

sub save_state {
    my ($file) = @_;

    my $device_list = get_bluetooth_status();
    if (!$debug) {
        verbose "saving Bluetooth status to $file";
        open my $fh, '>', $file
            or fatal "cannot save Bluetooth status to $file: $!";
        verbose "saving Bluetooth status to $file";
        print $fh JSON->new->pretty->encode($device_list);
        close $fh;
    }
    return $device_list;
}

sub power_bluetooth {
    my ($state, $device_list) = @_;

    verbose "powering $state Bluetooth devices";
    for my $dev_entry (@$device_list) {
        next unless $dev_entry->{powered};
        verbose $debug ? "[SKIP] " : "", "power $state $$dev_entry{device}";
        next if $debug;
        if (system('bluetoothctl', 'select', $$dev_entry{device}) != 0) {
            say STDERR "cannot select $$dev_entry{device} for power $state";
            next;
        }
        if (system('bluetoothctl', 'power', $state) != 0) {
            say STDERR "cannot power $state $$dev_entry{device}"; 
            next;
        }
        verbose "successfully powered $state $$dev_entry{device}"; 
    }
}

sub restore_state {
    my ($file) = @_;

    my $device_list;
    verbose "fetching saved device state from $file";
    if ($debug) {
        verbose "debug on, so emulate saved device state with actual state";
        $device_list = get_bluetooth_status();
    }
    else {
        open my $fh, '<', $file or fatal "cannot read $file: $!";
        my $device_str = do { local($/) = undef; <$fh> };
        close $fh;
        $device_list = JSON->new->decode($device_str);
        debug("Bluetooth status:\n", JSON->new->pretty->encode($device_list));
    }
    power_bluetooth('on', $device_list);
    verbose "removing $file";
    unlink $file or fatal "could not delete $file: $!";
}

Main();
exit 0;
