#!/usr/bin/env python3
"""
GTK application indicator that subscribes to a MQTT broker and sends
notifikations through libnotify.
Use ~/.mqtt2notify.yaml to configure the application
"""
__author__ = 'Jonas Forsberg <barregargamel@gmail.com>'
__licence__ = 'Apache License v2'


import os
import stat
import sys
import imp
import logging
import logging.config
import string
import threading
import random
import ssl
from shutil import copyfile
import paho.mqtt.client as paho_mqtt
import yaml
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk
gi.require_version('AppIndicator3', '0.1')
from gi.repository import AppIndicator3
from gi.repository import GObject
import time

CONFIG_FILE = os.path.expanduser("~/.mqttclient.yaml")
ICON_RUNNING = 'images/icon_running.png'
ICON_CONNECTED = 'images/icon_connected.png'
ICON_DISCONNECTED = 'images/icon_disconnected.png'
ICON_WARNING = 'images/icon_warning.png'
ICON_ERROR = 'images/icon_error.png'

APPINDICATOR_ID = 'MQTTClient'

DEFAULT_CONFIG_FILE = os.path.dirname(os.path.realpath(__file__)) + '/default_mqttclient.yaml'

MQTT_LOOP_TIMEOUT = 1.0
MQTT_LOOP_MAX_PACKETS = 1

def get_resource_path(rel_path):
    """ returns absolut path to file """
    dir_of_py_file = os.path.dirname(os.path.realpath(__file__))
    rel_path_to_resource = os.path.join(dir_of_py_file, rel_path)
    abs_path_to_resource = os.path.abspath(rel_path_to_resource)
    return abs_path_to_resource


class MQTT(threading.Thread):
    """ Thread to handle MQTT communication """
    def __init__(self, config):
        super(MQTT, self).__init__()
        self.running = True
        self.lost_connection = False
        self.config = config
        self.client_name = 'mqttclient_' + ''.join(
            random.choice(string.ascii_uppercase + string.digits) for _ in range(15))
        self.client = paho_mqtt.Client(self.client_name)
        if 'logger' in self.config:
            self.client.enable_logger(logging.getLogger(self.config['logger']))
        self.client.on_connect = self.on_connect
        self.client.on_disconnect = self.on_disconnect
        self.client.on_message = self.on_message
        self.client.username_pw_set(
            config['user'],
            config['password']
        )
        if 'use_tls' in self.config:
            if self.config['use_tls']:
                self.client.tls_set(self.config['ca_file'], tls_version=ssl.PROTOCOL_TLSv1_1)

    def on_connect(self, client, userdata, flags, rc):
        """ callback for paho mqtt client """
        if rc == 0:
            self.lost_connection= False
            GObject.idle_add(APP.connected)
            subs = []
            for subscription in self.config['subscriptions']:
                if subscription['subscribe']:
                    if 'qos' in subscription:
                        qos=subscription['qos']
                    else:
                        qos=0
                    subs.append((subscription['topic'], qos))
            self.client.subscribe(subs)
        else:
            LOG.debug('Connection problem')
            GObject.idle_add(APP.warning)

    def on_disconnect(self, client, userdata, rc=0):
        """ callback for paho mqtt client """
        self.lost_connection = False
        if rc == 0:
            GObject.idle_add(APP.disconnected)
        else:
            self.lost_connection = True
            LOG.warning('Lost connection with mqtt-broker: %s', str(rc))
            GObject.idle_add(APP.warning)

    def on_message(self, client, userdata, msg):
        """ callback for ipaho mqtt client """
        LOG.debug('Got message: topic=%s message=%s', msg.topic, msg.payload)
        GObject.idle_add(APP.got_message, msg)

    def quit(self):
        """ dissconnect paho mqtt client """
        self.client.disconnect()
        self.running = False

    def connect(self):
        self.lost_connection = False
        try:
            LOG.info('Connecting to %s:%i', self.config['host'], self.config['port'])
            self.client.connect(
                self.config['host'],
                self.config['port'],
                self.config['timeout_reconnect'])
        except Exception as error:
            LOG.error('Unable to connect to %s:%i  : %s',
                      self.config['host'], self.config['port'], str(error))
            GObject.idle_add(APP.error)

    def disconnect(self):
        self.client.disconnect()

    def run(self):
        if self.config['connect_on_launch']:
            self.connect()
        while self.running:
            if self.lost_connection:
                try:
                    self.client.reconnect()
                    time.sleep(10)
                except Exception as error:
                    LOG.error('Unable to connect to %s:%i  : %s',
                            self.config['host'], self.config['port'], str(error))
                    time.sleep(2)
            self.client.loop(MQTT_LOOP_TIMEOUT, MQTT_LOOP_MAX_PACKETS)

class MQTTNotify():
    """ Application class """
    def __init__(self, config):
        self.config = config
        self.mqtt = MQTT(config['mqtt'])
        self.plugins = {}
        self._load_plugins()

        # Create right-click menu
        self.menu = Gtk.Menu()
        self.publish_item = Gtk.MenuItem("Publish")
        self.seperator_item = Gtk.SeparatorMenuItem()
        self.connect_item = Gtk.MenuItem("Connect")
        self.disconnect_item = Gtk.MenuItem("Disonnect")
        self.quit_item = Gtk.MenuItem("Quit")
        self.menu.append(self.publish_item)
        self.menu.append(self.seperator_item)
        self.menu.append(self.connect_item)
        self.menu.append(self.disconnect_item)
        self.menu.append(self.quit_item)
        self._create_publish_menu(self.publish_item, self.config['publish'])
        #add callbacks
        self.quit_item.connect_object("activate", self.quit, "Quit")
        self.connect_item.connect_object("activate", self.connect, "Connect")
        self.disconnect_item.connect_object("activate", self.disconnect, "Disconnect")

        # create icon and icon menu
        self.indicator = AppIndicator3.Indicator.new(
                APPINDICATOR_ID, get_resource_path(ICON_DISCONNECTED),
                AppIndicator3.IndicatorCategory.SYSTEM_SERVICES
                )
        self.indicator.set_status(AppIndicator3.IndicatorStatus.ACTIVE)
        self.indicator.set_menu(self.menu)

        #Show the menu items
        self.publish_item.show()
        self.publish_item.set_sensitive(False)
        self.seperator_item.show()
        self.connect_item.show()
        self.disconnect_item.hide()
        self.quit_item.show()

    def _create_publish_menu(self, root_sub_menu_item, menu_content):
        """ create the publish menu on the right click """
        sub_menu = Gtk.Menu()
        for item in menu_content:
            if 'menu' in item:
                sub_menu_item = Gtk.MenuItem(item['name'])
                sub_menu.append(sub_menu_item)
                sub_menu_item.show()
                self._create_publish_menu(sub_menu_item, item['menu'])
            else:
                sub_menu_item = Gtk.MenuItem(item['name'])
                sub_menu.append(sub_menu_item)
                sub_menu_item.connect_object("activate", self.publish_item_response, item['name'])
                sub_menu_item.show()
        root_sub_menu_item.set_submenu(sub_menu)


    def _load_plugins(self):
        LOG.debug('Loading plugins..')
        plugin_list = get_plugins()
        for sub in self.config['mqtt']['subscriptions']:
            for plugin_name in sub['plugins']:
                for plugin in plugin_list:
                    if plugin['name'] == plugin_name:
                        self.plugins[plugin_name] = plugin

    def disconnect(self, data=None):
        GObject.idle_add(self.mqtt.disconnect)

    def connect(self, data=None):
        GObject.idle_add(self.mqtt.connect)

    def _loop_publish_menu(self, item_list, string):
        for item in item_list:
            if 'menu' in item:
                self._loop_publish_menu(item['menu'], string)
            elif item['name'] == string:
                if 'qos' in item:
                    qos=item['qos']
                else:
                    qos=0
                self.mqtt.client.publish(
                item['topic'],
                item['payload'],
                qos=qos,
                retain=item['retain'])

    def publish_item_response(self, string):
        LOG.debug('Invoked publish on item %s', string)
        self._loop_publish_menu(self.config['publish'], string)

    def on_right_click(self, data, event_button, event_time):
        self.menu.popup(None, None, None, event_button, event_time)

    def disconnected(self):
        self.publish_item.set_sensitive(False)
        self.connect_item.show()
        self.disconnect_item.hide()
        self.indicator.set_icon(get_resource_path(ICON_DISCONNECTED))

    def connected(self):
        self.publish_item.set_sensitive(True)
        self.connect_item.hide()
        self.disconnect_item.show()
        self.indicator.set_icon(get_resource_path(ICON_RUNNING))

    def error(self):
        self.indicator.set_icon(get_resource_path(ICON_ERROR))

    def warning(self):
        self.publish_item.set_sensitive(False)
        self.indicator.set_icon(get_resource_path(ICON_WARNING))

    def quit(self, data=None):
        self.mqtt.quit()
        Gtk.main_quit()
        LOG.debug('Exiting...')

    def got_message(self, msg):
        for sub in self.config['mqtt']['subscriptions']:
            if sub['subscribe'] and paho_mqtt.topic_matches_sub(sub['topic'], msg.topic):
                for plugin_name in sub['plugins']:
                    try:
                        plugin = load_plugin(self.plugins[plugin_name])
                        LOG.debug('Preparing plugin : %s', plugin_name)
                        cfg = None
                        if 'plugin' in self.config:
                            if plugin_name in self.config['plugin']:
                                cfg = self.config['plugin'][plugin_name]
                        args = plugin.prep(msg, cfg)
                        if 'runner' in args:
                            plugin = load_plugin(self.plugins[args['runner']])
                            plugin_name = args['runner']
                        LOG.debug('Running plugin: %s', plugin_name)
                        plugin.run(**args)
                    except Exception as error:
                        LOG.error('Error in plugin <%s>: %s ', plugin_name, str(error))

    def main(self):
        self.mqtt.start()
        Gtk.main()

def get_plugins(plugin_root=None, main_module='__init__'):
    """ returns a list of availible plugins
        this is a very crude plugin system, should be refactored
    """
    plugins = []
    if plugin_root is None:
        plugin_root = os.path.dirname(os.path.realpath(__file__))+"/plugins"
    for plugin_folder in os.listdir(plugin_root):
        plugin_path = os.path.join(plugin_root, plugin_folder)
        if not os.path.isdir(plugin_path) or not main_module + ".py" in os.listdir(plugin_path):
            continue
        LOG.debug('Found plugin: %s', plugin_folder)
        plugin_info = imp.find_module(main_module, [plugin_path])
        plugins.append({
            'name': plugin_folder,
            'main_module': main_module, 'plugin_info': plugin_info
            })
    return plugins


def load_plugin(plugin):
    """ returns the plugin as a module
        this is a very crude plugin system and should be refactored
    """
    LOG.debug('Loading plugin: %s', plugin['name'])
    return imp.load_module(plugin['main_module'], *plugin['plugin_info'])

def read_config(config_file):
    """ reads the yaml config_file and returns a dictionary """
    #check to see if config_file exists, if not offer to create it
    if not os.path.isfile(config_file):
        md = Gtk.Dialog(
            "No configuration found",
            Gtk.Window(),
            None,
            (Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL,
            Gtk.STOCK_OK, Gtk.ResponseType.OK)
            )
        label = Gtk.Label("No configuration file found, do you want to create a default configuration?")
        box = md.get_content_area()
        box.add(label)
        md.show_all()
        response = md.run()
        md.destroy()
        if response == Gtk.ResponseType.OK:
            write_config(config_file)
        sys.exit()

    with open(config_file, "r") as yaml_file:
        config = yaml.load(yaml_file,  Loader=yaml.FullLoader)
    return config

def write_config(destination_file, source_file=DEFAULT_CONFIG_FILE):
    """
    write the dictionary 'config' to the file 'config_file'
    if config=None, write the default configuration
    """
    copyfile(source_file, destination_file)
    os.chmod(destination_file, 0o600)
    md = Gtk.Dialog(
            "Configuration created",
            Gtk.Window(),
            None,
            (Gtk.STOCK_OK, Gtk.ResponseType.OK)
            )
    box = md.get_content_area()
    label = Gtk.Label( "Change your settings in ~/.mqttclient.yaml and start mqttclient again.")
    box.add(label)
    md.show_all()
    md.run()
    md.destroy()



def check_file_permission(filename, mode):
    file_permission = str(oct(stat.S_IMODE(os.stat(filename).st_mode)))
    if file_permission == mode:
        return True
    else:
        return False


if __name__ == '__main__':
    GObject.threads_init()
    #configure logging
    CONFIG = read_config(CONFIG_FILE)
    logging.config.dictConfig(CONFIG['logging'])
    LOG = logging.getLogger('mqttclient')
    LOG.debug('Starting')
    if not check_file_permission(CONFIG_FILE, '0600'):
        LOG.warning('Wrong permissions on file %s', CONFIG_FILE)

    APP = MQTTNotify(CONFIG)
    APP.main()
