#!/usr/bin/env python3

# barkery - digital signage based on WebKit2
#
# Authors:
#   Thomas Liske <thomas@fiasko-nw.net>
#
# Copyright Holder:
#   2015 - 2022 (C) Thomas Liske [http://fiasko-nw.net/~thomas/]
#
# License:
#   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; either version 2 of the License, or
#   (at your option) any later version.
#
#   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 package; if not, write to the Free Software
#   Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301 USA
#

import argparse
import configparser
import gi
import json
from setproctitle import setproctitle
import sys
import os
from paho.mqtt import client as mqtt_client
import platform
import time

# set process title
setproctitle("barkery")

# load Gtk3 & Gdk
gi.require_version('Gtk', '3.0')

# load Webkit2
gi.require_version('WebKit2', '4.1')

from gi.repository import WebKit2
from gi.repository import Gdk
from gi.repository import Gtk


class Barkery():
    def __init__(self, config_file=None):
        self.config = configparser.ConfigParser()
        if config_file:
            self.config.read_file(config_file)

        self.start_uri = self.config.get(
            'browser', 'start_uri', fallback='http://localhost')

        self.state = {
            'uri': self.start_uri,
            'state': 'init',
            'ts': int(time.time()),
        }

        preferred_languages = self.config.get('browser', 'preferred_languages', fallback='en-US').split(',')
        self.init_webkit(preferred_languages)
        if self.config.getboolean('mqtt', 'enabled', fallback=True):
            self.connect_mqtt()

    def publish_state(self, **kwargs):
        if not hasattr(self, 'client'):
            return

        self.state['ts'] = int(time.time())
        for key, value in kwargs.items():
            self.state[key] = value

        self.client.publish(
            topic=self.topics_publish['status'],
            payload=json.dumps(self.state),
            retain=True)

    def connect_mqtt(self):
        '''connect and subscribe to MQTT'''

        def on_connect(client, userdata, flags, rc):
            if rc == 0:
                print("MQTT: connected")

                for sub, callback in self.topics_subscribe.items():
                    print(f"MQTT: subscribe for {sub}")
                    client.message_callback_add(sub, callback)
                    client.subscribe(sub)

                self.publish_state(state='alive')
            else:
                print("MQTT: failed to connect ({})".format(rc))

        def on_disconnect(client, userdata, rc):
            print("MQTT: disconnected ({})".format(rc))

        def on_log(client, userdata, level, buf):
            print("MQTT: {}".format(buf))


        def cmd_load(client, userdata, msg):
            self.view.load_uri(msg.payload.decode('utf-8'))

        def cmd_reset(client, userdata, msg):
            self.view.load_uri(self.start_uri)

        def cb_screenshot_finished(view, task):
            surface = view.get_snapshot_finish(task)
            # ToDo: publish screenshot via MQTT

        def cmd_screenshot(client, userdata, msg):
            self.view.get_snapshot(WebKit2.SnapshotRegion.VISIBLE, WebKit2.SnapshotOptions.NONE, None, cb_screenshot_finished)

        def cmd_sendkeys(client, userdata, msg):
            # ToDo: implement sendkeys
            pass

        hostname = os.getenv('HOSTNAME', platform.node()).split('.')[0]
        self.topics_publish = {
            'status': self.config.get('mqtt', 'topic_status', fallback=f'barkery/terminal/{hostname}/status'),
            'screenshot': self.config.get('mqtt', 'topic_screenshot', fallback=f'barkery/terminal/{hostname}/screenshot'),
        }
        topic_ucast = self.config.get('mqtt', 'topic_ucast_cmd', fallback=f'barkery/terminal/{hostname}/cmd')
        topic_bcast = self.config.get('mqtt', 'topic_bcast_cmd', fallback='barkery/terminal/_bcast/cmd')
        self.topics_subscribe = {
            f'{topic_ucast}/load': cmd_load,
            f'{topic_bcast}/load': cmd_load,

            f'{topic_ucast}/reset': cmd_reset,
            f'{topic_bcast}/reset': cmd_reset,

            f'{topic_ucast}/sendkeys': cmd_sendkeys,
            f'{topic_bcast}/sendkeys': cmd_sendkeys,

            f'{topic_ucast}/screenshot': cmd_screenshot,
            f'{topic_bcast}/screenshot': cmd_screenshot,
        }

        self.client = mqtt_client.Client()
        self.client.on_connect = on_connect
        self.client.on_disconnect = on_disconnect
        self.client.on_log = on_log
        self.client.will_set(
            topic=self.topics_publish['status'],
            payload=json.dumps({
                'state': 'dead',
            }),
            retain=True)
        if self.config.get('mqtt', 'username', fallback=None):
            self.client.username_pw_set(
                self.config.get('mqtt', 'username'),
                self.config.get('mqtt', 'password', fallback=None))
        self.client.connect_async(
            self.config.get('mqtt', 'host', fallback='localhost'),
            self.config.getint('mqtt', 'port', fallback=1884))
        self.client.loop_start()

    def init_webkit(self, preferred_languages):
        '''initialize and present main window and webview'''

        def window_destroyed(obj):
            '''terminate barkery if the window is destroyed'''
            Gtk.main_quit()

        def view_changed(view, event):
            self.publish_state(uri=view.get_uri(), load=event)
            if event == WebKit2.LoadEvent.FINISHED:
                print(f"WebKit: view changed to {view.get_uri()}")

        def view_failed(view, event, uri, error):
            print(f"WebKit: failed to load {uri} due to {error}")

        def view_crashed(view):
            print('WebKit: web process crashed')
            time.sleep(5)
            self.view.load_uri(self.start_uri)


        Gtk.init(sys.argv)

        screen = Gdk.Screen.get_default()
        window = Gtk.Window.new(Gtk.WindowType.TOPLEVEL)
        monitor = screen.get_display().get_monitor_at_window(screen.get_root_window())
        geometry = monitor.get_geometry()
        window.set_role('browser')
        window.set_title("Barkery")
        window.set_icon_name('text-html')
        window.set_default_size(geometry.width, geometry.height)

        scrolls = Gtk.ScrolledWindow.new()
        self.view = WebKit2.WebView.new()
        scrolls.add(self.view)

        settings = self.view.get_settings()
        settings.set_property('enable-write-console-messages-to-stdout', True)
        settings.set_property('enable-fullscreen', False)

        context = WebKit2.WebContext.get_default()
        context.set_preferred_languages(preferred_languages)

        window.add(scrolls)
        window.show_all()
        window.present()
        window.connect('destroy', window_destroyed)
        window.fullscreen()

        # register WebView callbacks
        self.view.connect('load-changed', view_changed)
        self.view.connect('load-failed', view_failed)
        self.view.connect('web-process-crashed', view_crashed)

        # load initial site
        self.view.load_uri(self.start_uri)

        # give view focus to enable scrolling by keyboard
        self.view.grab_focus()

    def main(self):
        '''start Gtk main loop (blocking)'''

        print("starting Gtk main loop...")
        Gtk.main()


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument("-c", "--config", type=str, default="/etc/barkery/barkery.conf",
                    help="configuration file")
    args = parser.parse_args()

    with open(args.config, 'r') as f:
        barkery = Barkery(config_file=f)
    barkery.main()
