#!/usr/bin/python3
# -*- coding: utf-8 -*-

# Copyright (C) 2016 Canonical Ltd.
# Author: Christopher Townsend <christopher.townsend@canonical.com>

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; version 3 of the License.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.

import dbus
import dbus.service
import libertine.LxcContainer
import libertine.utils
import os
import shlex
import signal
import subprocess

from collections import Counter
from dbus.mainloop.glib import DBusGMainLoop
from gi.repository import GLib
from libertine.ContainersConfig import ContainersConfig


home_path = os.environ['HOME']

LIBERTINE_LXC_MANAGER_NAME = libertine.LxcContainer.get_lxc_manager_dbus_name()
LIBERTINE_LXC_MANAGER_PATH = libertine.LxcContainer.get_lxc_manager_dbus_path()


class Service(dbus.service.Object):

    def __init__(self):
        self.is_pulse_setup = False
        self.app_counter = Counter()
        self.containers_config = ContainersConfig()
        self.operation_counter = Counter()

        DBusGMainLoop(set_as_default=True)
        try:
            bus_name = dbus.service.BusName(LIBERTINE_LXC_MANAGER_NAME,
                                            bus=dbus.SessionBus(),
                                            do_not_queue=True)
        except dbus.exceptions.NameExistsException:
            print("service is already running")
            raise
        super().__init__(bus_name, LIBERTINE_LXC_MANAGER_PATH)

    @dbus.service.method(LIBERTINE_LXC_MANAGER_NAME,
                         in_signature='ss',
                         out_signature='(bs)')
    def app_start(self, container_id, lxc_logfile):
        if self.operation_counter[container_id] != 0:
            return (False, "Libertine container operation already running: cannot launch application.")

        (started, reason) = self._launch_lxc_container(container_id, lxc_logfile)

        if started:
            self.app_counter[container_id] += 1

        return (started, reason)

    @dbus.service.method(LIBERTINE_LXC_MANAGER_NAME,
                         in_signature='s')
    def app_stop(self, container_id):
        self.app_counter[container_id] -= 1

        if self.app_counter[container_id] == 0:
            self._stop_lxc_container(container_id)
            del self.app_counter[container_id]

    @dbus.service.method(LIBERTINE_LXC_MANAGER_NAME,
                         in_signature='ss',
                         out_signature='(bs)')
    def operation_start(self, container_id, lxc_log_file):
        if self.app_counter[container_id] != 0:
            return (False, "Application already running in container: cannot run operation.")

        (started, reason) = self._launch_lxc_container(container_id, lxc_log_file, launchable=False)

        if started:
            self.operation_counter[container_id] += 1

        return (started, reason)

    @dbus.service.method(LIBERTINE_LXC_MANAGER_NAME,
                         in_signature='s')
    def operation_stop(self, container_id):
        self.operation_counter[container_id] -= 1

        if self.operation_counter[container_id] == 0:
            self._stop_lxc_container(container_id)
            del self.operation_counter[container_id]

    def _setup_pulse(self):
        pulse_socket_path = os.path.join(libertine.utils.get_libertine_runtime_dir(), 'pulse_socket')

        lsof_cmd = 'lsof -n %s' % pulse_socket_path
        args = shlex.split(lsof_cmd)
        lsof = subprocess.Popen(args, stderr=subprocess.DEVNULL, stdout=subprocess.DEVNULL)
        lsof.wait()

        if not os.path.exists(pulse_socket_path) or lsof.returncode == 1:
            pactl_cmd = (
                'pactl load-module module-native-protocol-unix auth-anonymous=1 socket=%s'
                % pulse_socket_path)
            args = shlex.split(pactl_cmd)
            subprocess.Popen(args).wait()

        self.is_pulse_setup = True

    def _launch_lxc_container(self, container_id, lxc_log_file, launchable=True):
        container = libertine.LxcContainer.lxc_container(container_id)

        if not container.defined:
            return (False, "Container {} is not valid".format(container_id))

        if launchable and not self.is_pulse_setup:
            self._setup_pulse()

        if not container.running:
            if launchable:
                self._dynamic_bind_mounts(container, container_id)

            return libertine.LxcContainer.lxc_start(container, lxc_log_file)

        return (True, "")

    def _stop_lxc_container(self, container_id):
        container = libertine.LxcContainer.lxc_container(container_id)

        libertine.LxcContainer.lxc_stop(container)

    def _dynamic_bind_mounts(self, container, container_id):
        self.containers_config.refresh_database()
        mounts = self._sanitize_bind_mounts(libertine.utils.get_common_xdg_user_directories() + \
                                            self.containers_config.get_container_bind_mounts(container_id))

        data_dir = libertine.utils.get_libertine_container_userdata_dir_path(container_id)
        for user_dir in libertine.utils.generate_binding_directories(mounts, home_path):
            user_dir_fullpath = os.path.join(data_dir, user_dir[1])
            if not os.path.exists(user_dir_fullpath):
                os.makedirs(user_dir_fullpath, exist_ok=True)

            xdg_user_dir_entry = (
                "%s %s/%s none bind,create=dir,optional"
                % (user_dir[0], home_path.strip('/'), user_dir[1])
            )
            container.append_config_item("lxc.mount.entry", xdg_user_dir_entry)

    def _sanitize_bind_mounts(self, mounts):
        return [mount.replace(" ", "\\040") for mount in mounts]


def sigterm(self):
    shutdown()


def shutdown():
    GLib.MainLoop().quit()


def main():
    service = Service()
    GLib.unix_signal_add(GLib.PRIORITY_HIGH,
                         signal.SIGTERM,
                         sigterm,
                         None)

    try:
        GLib.MainLoop().run()
    except KeyboardInterrupt:
        shutdown()


if __name__ == '__main__':
    main()
