#!/usr/bin/perl



use POSIX qw(:signal_h :sys_wait_h);
use lib qw(/usr/lib/libDrakX);
# i18n: IMPORTANT: to get correct namespace (drakx-net instead of libDrakX)
BEGIN { unshift @::textdomains, 'drakx-net' }
BEGIN { $ENV{GDK_BACKEND} = 'x11' if $ENV{WAYLAND_DISPLAY} }
use c;
use common;
use standalone;
use network::network;
use network::tools;
use network::connection;
use network::connection::ethernet;
use network::vpn;
use run_program;
use do_pkgs;
use mygtk3 qw(gtknew gtkset);
use dbus_object;
use network::monitor;
use network::signal_strength;
use detect_devices;
BEGIN { POSIX::sigprocmask(SIG_BLOCK, POSIX::SigSet->new(SIGCHLD)); }
use Gtk3::Notify -init, 'NetApplet';
use ugtk3 qw(:create :helpers :wrappers :dialogs);
use Glib::Object::Introspection;

my $onstartupfile = "$ENV{HOME}/.net_applet";
my $system_file = '/etc/sysconfig/drakx-net';
shouldStart() or die "$onstartupfile should be set to TRUE or use net_applet --force\n";

#- Allow multiple instances, but only one per user:
is_running('net_applet') and die "net_applet already running\n";

package network::net_applet;

use mygtk3 qw(gtknew gtkset);
use common;

our ($current_state, $current_interface);
our ($icon, $is_sni);
our $dbus;
our ($interactive_cb, $ifw, $ifw_alert);

our %wireless_networks;

our %pixbufs =
  (
      state => { map { $_ => gtknew('Pixbuf', file => $_ . '_big') } qw(connected disconnected unconfigured connecting) },
      encryption => { map { $_ => gtknew('Pixbuf', file => "encryption-$_-24") } qw(open weak strong) },
  );

sub get_current_network() {
    detect_devices::is_wireless_interface($current_interface) && find { $_->{current} } values %wireless_networks;
}

sub get_state_pixbuf {
    if (my $wnet = $current_state eq 'connected' && get_current_network()) {
        return network::signal_strength::get_strength_icon($wnet);
    }
    else {
        return $pixbufs{state}{$current_state};
    }
}

sub update_tray_icon() {
    if (!$ifw_alert) {
        if (my $pixbuf = get_state_pixbuf()) {
            my @args = $is_sni ? qw(STATUS_NOTIFIER_ICON) : ();
            push @args, $pixbuf;
            $icon->set_from_pixbuf(@args);
        }
    }
    elsif ($is_sni) {
        $icon->set_from_icon_name('STATUS_NOTIFIER_ICON', 'dialog-warning');
    }
    else {
        $icon->set_from_stock('gtk-dialog-warning');
    }
}

1;

package main;

my ($current_description, $simple_menu, $menu, $wireless_device, $timeout, $update_timeout);
add_icon_path("/usr/share/libDrakX/pixmaps/");

my $net = {};
my $watched_interface;

my %global_settings = getVarsFromSh($system_file);

sub get_state_message {
    my ($o_interface) = @_;
    my $interface = $o_interface || $current_interface;
    my $network = network::net_applet::get_current_network();
    formatAlaTeX(
        $current_state eq 'connected' ?
          N("Network is up on interface %s.", get_interface_name($interface)) .
          "\n\n" . N("IPv6 address: %s", network::tools::get_interface_ip6_address($net, $interface)) .
          "\n\n" . N("IP address: %s", network::tools::get_interface_ip_address($net, $interface)) .
          "\n\n" . N("Gateway: %s", [ network::tools::get_interface_status($interface) ]->[1]) .
          "\n\n" . N("DNS: %s", $net->{resolv}{dnsServer}) .
            ($network && "\n\n" . N("Connected to %s (link level: %d %%)", $network->{name}, $network->{signal_strength}))
        : $current_state eq 'disconnected' ?
          N("Network is down on interface %s.", get_interface_name($interface))
        : $current_state eq 'unconfigured' ?
          N("You do not have any configured Internet connection.
Run the \"%s\" assistant from the Mageia Linux Control Center", N("Set up a new network interface (LAN, ISDN, ADSL, ...)"))
        :
         N("Connecting...")
    );
}

sub get_interface_type {
    my ($interface) = @_;
    my $ifcfg = $net->{ifcfg}{$interface};
    require network::connection;
    $ifcfg && network::connection->find_ifcfg_type($ifcfg);
}

sub get_interface_icon {
    my ($interface) = @_;
    my $type = get_interface_type($interface);
    $type && $type->get_type_icon;
}

sub get_interface_name {
    my ($interface) = @_;
    my $type = get_interface_type($interface);
    my $type_name = $type && $type->get_type_description;
    $type_name ? "$type_name ($interface)" : $interface;
}

my %actions = (
    'upNetwork' => {
	name => sub { N("Connect %s", get_interface_name($_[0])) },
	launch => sub { network::tools::start_interface($_[0], 1) }
    },
    'downNetwork' => {
	name => sub { N("Disconnect %s", get_interface_name($_[0])) },
	launch => sub { network::tools::stop_interface($_[0], 1) }
    },
    'monitorNetwork' => {
	name => N("Monitor Network"),
	launch => \&run_net_monitor
    },
    'monitorIFW' => {
	name => N("Interactive Firewall"),
	launch => \&run_drakids
    },
    'wireless' => {
	name => N("Manage wireless networks"),
	launch => sub { run_drakroam() }
    },
    'drakvpn' => {
        name => N("Manage VPN connections"),
	launch => sub { run_program::raw({ detach => 1 }, 'drakvpn') },
    },
    'confNetwork' => {
	name => N("Configure Network"),
	launch => sub { run_program::raw({ detach => 1 }, 'drakconnect') }
    },
    'chooseInterface' => {
        name => N("Watched interface"),
        choices => sub { N("Auto-detect"), sort keys %{$net->{ifcfg}} },
        choice_selected => sub {
	    $watched_interface ? $_[0] eq $watched_interface :
	        $_[0] eq N("Auto-detect");
	},
        launch => sub {
            $watched_interface = $_[0] eq N("Auto-detect") ? undef : $_[0];
            checkNetworkForce();
        }
    },
    'setInterface' => {
        name => N("Active interfaces"),
        use_checkbox => 1,
        choices => sub { sort keys %{$net->{ifcfg}} },
        choice_selected => sub {
            my ($is_up, $_gw) = network::tools::get_interface_status($_[0]);
            $is_up;
        },
        format_choice => \&get_interface_name,
        get_icon => \&get_interface_icon,
        launch => sub {
            my ($is_up, $_gw) = network::tools::get_interface_status($_[0]);
            if ($is_up) {
                network::tools::stop_interface($_[0], 1);
            } else {
                network::tools::start_interface($_[0], 1);
            }
            checkNetworkForce();
        }
    },
    'chooseProfile' => {
        name => N("Profiles"),
        choices => sub { network::network::netprofile_list() },
        choice_selected => sub { $_[0] eq $net->{PROFILE} },
        launch => sub {
	    require run_program;
	    $net->{PROFILE} = $_[0];
	    run_program::raw({ detach => 1 }, (if_($>, '/usr/bin/pkexec'), '/usr/sbin/set-netprofile', $net->{PROFILE}));
        }
    },
    'chooseVPN' => {
        name => N("VPN connection"),
        header => "drakvpn",
        choices => sub {
	    map { $_->get_configured_connections } network::vpn::list_types;
        },
        allow_single_choice => 1,
        format_choice => \&network::vpn::get_label,
        choice_selected => sub { $_[0]->is_started },
        launch => sub {
	    require interactive; $_[0]->is_started ?
	        $_[0]->stop : $_[0]->start(interactive->vnew);
	},
    },
    'help' => {
	name => N("Help"),
	launch => sub { run_program::raw({ detach => 1 }, 'drakhelp', '--id', 'internet-connection') }
    },
    'quit' => {
	name => N("Quit"),
	launch => \&mainQuit
    },
);

# Plasma and Enlightenment support SNI natively. Other DE do not. In that case, we fall back to
# the deprecated GtkStatusIcon.
if ($ENV{DESKTOP_SESSION} =~ /plasma$|enlightenment/i) {
    $is_sni = 1;

    Glib::Object::Introspection->setup(
        basename => 'StatusNotifier',
        version => '1.0',
        package => 'StatusNotifier');

    $icon = StatusNotifier::Item->new_from_icon_name('net_applet', 'STATUS_NOTIFIER_CATEGORY_APPLICATION_STATUS', 'drakx-net-unconfigured');
    $icon->set_title('net_applet');
    $icon->set_status('STATUS_NOTIFIER_STATUS_ACTIVE');
    $icon->register;
}
else {
    $icon = Gtk3::StatusIcon->new;
    $icon->signal_connect(popup_menu => sub {
                              my ($_icon, $button, $time) = @_;
                              $menu->popup(undef, undef, undef, undef, $button, $time) if $menu;
                          });
}

$icon->signal_connect(activate => sub {
        if ($ifw_alert) {
            run_drakids();
        } elsif ($simple_menu) {
            $simple_menu->popup(undef, undef, undef, undef, 0, Gtk3::get_current_event_time());
        } else {
            run_netcenter();
        }
    });

eval { $dbus = dbus_object::system_bus() } if !defined($global_settings{NET_APPLET_DBUS}) || text2bool($global_settings{NET_APPLET_DBUS});
if (my $err = $@) {
	log::explanations("failed to acquire DBus: $err");
}
eval { $net->{monitor} = network::monitor->new($dbus) } if $dbus;
if (my $err = $@) {
	log::explanations("failed to acquire monitor on DBus: $err (is mandi started?)");
}
if ($dbus) {
    require network::net_applet::ifw;
    network::net_applet::ifw::init();
}
if ($dbus) {
    $dbus->{connection}->add_filter(sub {
        my ($_con, $msg) = @_;
            if ($msg->get_interface eq 'org.mageia.network' && $msg->get_member eq 'status') {
                my ($status, $interface) = $msg->get_args_list;
                print "got connection status event: $status $interface\n";
                if ($status eq "add") {
                    checkNetworkForce();
                }
            }
    });
    $dbus->{connection}->add_match("type='signal',interface='org.mageia.network'");
    dbus_object::set_gtk3_watch_helper($dbus);
}

checkNetworkForce();
cronNetwork();
gtkflush(); #- for notifications to appear on the status icon position
network::net_applet::ifw::get_unprocessed_ifw_messages() if $ifw;

$SIG{HUP} = sub {
    print "received SIGHUP, reloading network configuration\n";
    checkNetworkForce();
};
$SIG{USR1} = sub {
    # clear all ifw notifications
    eval { $network::net_applet::ifw->get_reports };
};

# NOTE: Must be called after checkNetworkForce() above to make sure that
# the menu has already been created, else set_context_menu() will complain.
# When net_applet is launched automatically when opening a new session, there
# seems to be some latency with DBus and/or DBusMenu, and when this method
# is called too early, it seems to be blocked. So we add a timer here to call
# it again to make sure that right-click events will be correctly handled by
# DBusMenu.
Glib::Timeout->add(100, sub { $icon->set_context_menu($menu); 0 }) if $is_sni;

# do not create zombies (#20552)
Glib::Timeout->add_seconds(1, sub {
			       POSIX::sigprocmask(SIG_UNBLOCK, POSIX::SigSet->new(SIGCHLD));
			       $SIG{CHLD} = \&harvester;
			       0;
			   });
sub harvester {
    my $pid;
    do {
        # we don't care about our child processes
        $pid = waitpid(-1, &WNOHANG);
    } while $pid > 0;
}

Gtk3->main;

ugtk3::exit(0);

sub is_running {
    my ($name, $o_user) = @_;
    my $user = $o_user || $ENV{USER};
    any {
	my ($ppid, $pid, $n) = /^\s*(\d+)\s+(\d+)\s+(.*)/;
	$ppid != 1 && $pid != $$ && $n eq $name;
    } `ps -o '%P %p %c' -u $user`;
}

sub is_running_match {
    # (eugeni) this matches part of a running command.
    # Right now it is only used to detect if ifup script is running.
    my ($name, $o_user) = @_;
    my $user = $o_user || $ENV{USER};
    any {
	my ($ppid, $pid, $n) = /^\s*(\d+)\s+(\d+)\s+(.*)/;
	$ppid != 1 && $pid != $$ && $n =~ $name;
    } `ps -o '%P %p %c' -u $user`;
}

sub shouldStart() {
    my ($opt) = @ARGV;
    if ($opt eq '--force' || $opt eq '-f') {
        return 1;
    }
    return getAutoStart();
}
sub run_net_monitor() {
    # stop reaping child processes or else gurpmi segfaults
    local $SIG{CHLD} = "DEFAULT";
    do_pkgs->do_pkgs->install('net_monitor');
    run_program::raw({ detach => 1 }, 'net_monitor', '--defaultintf', $current_interface) unless is_running('net_monitor');
}
sub run_netcenter() {
    run_program::raw({ detach => 1 }, 'draknetcenter') unless is_running('draknetcenter', 'root');
}
sub run_drakroam {
    my ($o_ap) = @_;
    run_program::raw({ detach => 1 }, 'drakroam', if_($o_ap, "--ap=$o_ap")) unless is_running('drakroam', 'root');
}
sub run_drakids() {
    $ifw_alert = 0;
    if (is_running('drakids')) {
        eval { $ifw->send_manage_request };
    } else {
        run_program::raw({ detach => 1 }, 'drakids');
    }
}
sub generate_wireless_menuitem {
    my ($wnet) = @_;
    my $menuitem = {};
    $menuitem->{widget} = Gtk3::CheckMenuItem->new;
    $menuitem->{widget}->set_draw_as_radio(1);
    $menuitem->{widget}->add(gtkpack_(gtkshow(gtknew('HBox')),
                                            1, gtkset_alignment($menuitem->{label} = gtknew('Label'), 0, 0.5),
                                            0, $menuitem->{strength} = Gtk3::Image->new,
                                            0, $menuitem->{security} = Gtk3::Image->new,
                                ));
    $menuitem->{activate} = $menuitem->{widget}->signal_connect('activate' => sub {
        if ($net->{monitor} && exists $wnet->{id}) {
            eval { $net->{monitor}->select_network($wnet->{id}) };
            $@ and err_dialog(N("Wireless networks"), N("Unable to contact daemon"));
        } else {
            run_drakroam($wnet->{ap});
        }
        checkNetworkForce();
    });
    update_wireless_item($menuitem, $wnet);
    push @{$wnet->{menuitems}}, $menuitem;
    return $menuitem->{widget};
}
sub update_wireless_item {
    my ($menuitem, $wnet) = @_;
    $menuitem->{label}->set_text($wnet->{name});
    $menuitem->{security}->set_from_pixbuf($pixbufs{encryption}{$wnet->{flags} =~ /WPA/i ? 'strong' : $wnet->{flags} =~ /WEP/i ? 'weak' : 'open'});
    $menuitem->{strength}->set_from_pixbuf(scalar network::signal_strength::get_strength_icon($wnet));

    $menuitem->{widget}->signal_handler_block($menuitem->{activate});
    $menuitem->{widget}->set_active($wnet->{current});
    $menuitem->{widget}->signal_handler_unblock($menuitem->{activate});
}
sub checkWireless() {
    $wireless_device or return;
    my ($networks) =  network::monitor::list_wireless($net->{monitor});
    my $force_applet_update;
    foreach (keys %$networks) {
        exists $wireless_networks{$_} or $force_applet_update = 1;
        put_in_hash($wireless_networks{$_} ||= {}, $networks->{$_});
    }
    if ($force_applet_update) {
        undef $current_state;
    } else {
        foreach my $wnet (values %wireless_networks) {
            my $is_valuable = exists $networks->{$wnet->{ap}};
            foreach (@{$wnet->{menuitems}}) {
                update_wireless_item($_, $wnet) if $is_valuable;
                $_->{widget}->set_visible($is_valuable);
            }
        }
    }
}
sub checkNetwork() {
    my ($gw_intf, $_is_up, $gw_address) = $watched_interface ?
      ($watched_interface, network::tools::get_interface_status($watched_interface)) :
      network::tools::get_default_connection($net);
    my $connecting = is_running_match('ifup', 'root');
    go2State($gw_address ? 'connected' : $connecting ? 'connecting' : $gw_intf ? 'disconnected' : 'unconfigured', $gw_intf);
}
sub checkNetworkForce() {
    $net = {};
    network::network::read_net_conf($net);
    undef $current_state;
    $wireless_device = detect_devices::get_wireless_interface();
    checkWireless();
    checkNetwork();
}
sub cronNetwork() {
    my $i;
    $timeout = Glib::Timeout->add_seconds(2, sub {
        checkWireless() if !($i++%30);
        checkNetwork();
        1;
    });
}
sub go2State {
    my ($state_type, $interface) = @_;
    my $need_update;
    my ($old_interface, $old_description);
    if ($current_interface ne $interface) {
        my $card = find { $_->[0] eq $interface } network::connection::ethernet::get_eth_cards();
        if ($state_type eq 'disconnected') {
            $old_interface = $current_interface;
            $old_description = $current_description;
        }
        $current_description = $card && $card->[2];
        $current_interface = $interface;
        $need_update = 1;
    }
    my $show;
    if ($current_state ne $state_type) {
        $show = defined $current_state && $state_type ne 'connecting'; # don't show notification at applet startup and when establishing a connection
        $current_state = $state_type;
    }
    if ($show) {
	my $msg;
        my $bubble = Gtk3::Notify::Notification->new(
		$old_description || $current_description || N("Network connection"),
		get_state_message($old_interface || $current_interface));
        my $pixbuf = network::net_applet::get_state_pixbuf();
        $bubble->set_icon_from_pixbuf($pixbuf) if $pixbuf;

        my $timeout = 5000;
        $bubble->set_timeout($timeout);
        # both need to be in a eval block in case notification daemon isn't running:
        Glib::Timeout->add_seconds($timeout/1000, sub { eval { $bubble->close }; 0 });
        eval { $bubble->show };
        warn ">> ERR:$@" if $@;
        $need_update = 1;
    }

    update_applet() if $need_update;
}

sub update_applet() {
    $wireless_device = detect_devices::get_wireless_interface();

    # Re-checking wireless networks (#40912)
    checkWireless();

    generate_menu();

    network::net_applet::update_tray_icon();
    my $message = get_state_message();

    $is_sni ? $icon->set_tooltip("drakx-net-$current_state", 'net_applet', $message) : $icon->set_tooltip_text($message);
}

sub create_menu_choices {
    my ($action, $o_allow_single_choice) = @_;
    my @choices =  $actions{$action}{choices}->();
    #- don't add submenu if only zero or one choice exists
    my $allow_single_choice = $actions{$action}{allow_single_choice} || $o_allow_single_choice;
    @choices > ($allow_single_choice ? 0 : 1) or return ();
    my $selected = $actions{$action}{choice_selected};
    my $format = $actions{$action}{format_choice};
    my $get_icon = $actions{$action}{get_icon};
    map {
        my $choice = $_;
        my $label = $format ? $format->($choice) : $choice;
        my $w = gtkshow(gtkset_active(gtkadd(
            Gtk3::CheckMenuItem->new,
            gtknew('HBox', children => [
                     1, gtkset_alignment(gtknew('Label', text => $label), 0, 0.5),
                     $get_icon ?
                       (0, gtknew('Image', file => $get_icon->($_))) :
                         (),
                 ])), $selected->($choice) || 0));
        gtksignal_connect($w, activate => sub { $actions{$action}{launch}->($choice) });
        $w->set_draw_as_radio(!$actions{$action}{use_checkbox});
        $w;
    } $actions{$action}{choices}->();
}

sub create_action_item {
    my ($action) = @_;
    my $name = ref($actions{$action}{name}) eq 'CODE' ? $actions{$action}{name}->($current_interface) : $actions{$action}{name};
    if (exists $actions{$action}{choices}) {
        my @menu = create_menu_choices($action);
        @menu || $actions{$action}{header} or return ();
        gtkshow(create_menu($name,
                            $actions{$action}{header} ? (
                                create_action_item($actions{$action}{header}),
                                gtkshow(Gtk3::SeparatorMenuItem->new),
                            ) : (),
                            @menu,
                        ));
    } else {
        gtksignal_connect(gtkshow(Gtk3::MenuItem->new_with_label($name)), activate => sub { $actions{$action}{launch}->($current_interface) });
    }
}

sub get_wireless_networks_sorted() {
    sort {
        $b->{current} <=> $a->{current} || $b->{signal_strength} <=> $a->{signal_strength} || $a->{name} cmp $b->{name};
    } values %wireless_networks;
}

sub generate_menu() {
    delete $_->{menuitems} foreach values %wireless_networks;
    # Unset the context menu before destroying it.
    $icon->set_context_menu() if $is_sni;
    $menu->destroy if $menu;
    $menu = Gtk3::Menu->new;

    my (@settings);
    my $interactive;
    eval { $interactive = $ifw->get_interactive };

    if ($current_state eq 'connected') {
        $menu->append(create_action_item($_)) foreach qw(downNetwork monitorNetwork);
    } elsif ($current_state eq 'disconnected') {
        $menu->append(create_action_item('upNetwork'));
    }
    $menu->append(create_action_item('monitorIFW')) if $current_state ne 'unconfigured' && defined $interactive;

    $menu->append(create_action_item('confNetwork'));

    push @settings, create_action_item('chooseInterface') if $current_state ne 'unconfigured';

    push @settings, create_action_item('chooseProfile');
    if (defined $interactive) {
        $interactive_cb = gtkshow(gtksignal_connect(gtkset_active(Gtk3::CheckMenuItem->new_with_label(N("Interactive Firewall automatic mode")),
                                                                  !$interactive),
                                                    toggled => sub { eval { $ifw->set_interactive(to_bool(!$_[0]->get_active)) } }));
        push @settings, $interactive_cb;
    }
    push @settings, gtkshow(gtksignal_connect(gtkset_active(Gtk3::CheckMenuItem->new_with_label(N("Always launch on startup")), getAutoStart()),
                                              toggled => sub { setAutoStart(uc(bool2text($_[0]->get_active))) }));

    $menu->append(gtkshow(Gtk3::SeparatorMenuItem->new));
    if ($current_state ne 'unconfigured' && $wireless_device) {
        $menu->append(gtkshow(create_menu(N("Wireless networks"),
                                          create_action_item('wireless'),
                                          gtkshow(Gtk3::SeparatorMenuItem->new),
                                          map { generate_wireless_menuitem($_) } get_wireless_networks_sorted())));
    }
    if (my $vpn = create_action_item('chooseVPN')) { $menu->append($vpn) }
    if (my $set = $current_state ne 'unconfigured' && create_action_item('setInterface')) { $menu->append($set) }
    $menu->append(gtkshow(create_menu(N("Settings"), @settings)));
    $menu->append(gtkshow(Gtk3::SeparatorMenuItem->new));
    $menu->append(create_action_item('help'));
    $menu->append(create_action_item('quit'));
    # As we destroyed the menu before recreating it, we need to attach it again.
    $icon->set_context_menu($menu) if $is_sni;
}
sub mainQuit() {
     Glib::Source->remove($timeout) if $timeout;
     Glib::Source->remove($update_timeout) if $update_timeout;
     Gtk3->main_quit;
}
sub getAutoStart() {
    my %p = getVarsFromSh($onstartupfile);
    return to_bool($p{AUTOSTART} ne 'FALSE');
}
sub setAutoStart {
    my $state = shift;
    output_p $onstartupfile,
    qq(AUTOSTART=$state
);
}
