#!/usr/bin/env python3

"""
    SMA-Screensaver - Sunny Portal screensaver
    Fork of WebScreensaver: https://github.com/lmartinking/webscreensaver
    Fork repository: https://gitea.diehanis.de/Winz/SMA-Screensaver
    Modified 2026-09-29: fixed Sunny Portal URL, automatic SMA login,
    local JSON credentials and persistent cookies; removed demo selection.
    Copyright (C) 2012-2017  Lucas Martin-King & Thomas Reifenberger

    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 program; if not, write to the Free Software Foundation, Inc.,
    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
"""

import os
import signal
import json
from pathlib import Path
from urllib.parse import urlsplit

import gi
gi.require_version('Gtk', '3.0')
gi.require_version('GdkX11', '3.0')
gi.require_version('WebKit2', '4.1')

from gi.repository import Gtk, GdkX11
from gi.repository import WebKit2 as WebKit


SUNNY_PORTAL_URL = "https://ennexos.sunnyportal.com/login"
CREDENTIALS_FILE = Path.home() / ".config/sma-screensaver/credentials.json"
COOKIE_FILE = Path.home() / ".local/share/sma-screensaver/cookies.txt"

SMA_LOGIN_SCRIPT = """
(credentials => {
    if (location.origin !== 'https://login.sma.energy') return false;
    const username = document.getElementById('username');
    const password = document.getElementById('password');
    const form = password && password.form;
    const button = form && form.querySelector('button[name="login"][type="submit"]');
    if (!username || !form || username.form !== form || !button || button.disabled ||
        new URL(form.action, location.href).origin !== location.origin) return false;

    for (const [field, value] of [[username, credentials.username], [password, credentials.password]]) {
        field.value = value;
        field.dispatchEvent(new Event('input', {bubbles: true}));
        field.dispatchEvent(new Event('change', {bubbles: true}));
    }
    if (!form.checkValidity()) return false;
    button.click();
    return true;
})
"""


def load_credentials():
	try:
		credentials = json.loads(CREDENTIALS_FILE.read_text(encoding='utf-8'))
	except (OSError, ValueError):
		raise ValueError(f"Zugangsdaten fehlen oder JSON ist nicht lesbar: {CREDENTIALS_FILE}") from None
	if not isinstance(credentials, dict) or any(
		not isinstance(credentials.get(key), str) or not credentials[key].strip()
		for key in ('username', 'password')
	):
		raise ValueError('credentials.json muss username und password als nicht leere Zeichenketten enthalten.')
	return {key: credentials[key] for key in ('username', 'password')}

# The portal renders its login button asynchronously through Angular.
PORTAL_LOGIN_SCRIPT = """
(() => {
    if (location.origin !== 'https://ennexos.sunnyportal.com' ||
        location.pathname.replace(/\\/$/, '') !== '/login') return;

    let attempts = 0;
    const timer = setInterval(() => {
        if (location.pathname.replace(/\\/$/, '') !== '/login') {
            clearInterval(timer);
            return;
        }
        const button = document.querySelector('ennexos-button#login button, button#login');
        if (button && !button.disabled &&
            button.getAttribute('aria-disabled') !== 'true' &&
            button.getClientRects().length > 0) {
            clearInterval(timer);
            button.click();
        } else if (++attempts >= 120) {
            clearInterval(timer);
            console.warn('Sunny Portal: login button unavailable after 60 seconds.');
        }
    }, 500);
})();
"""


class WebScreensaver(object):
	'''
	A simple wrapper for WebKit which works as an XScreensaver Hack
	'''

	def __init__(self, window_id=None, disk_cache=True):
		self.window_id = window_id
		self.disk_cache = disk_cache
		self.login_attempted = False

		self.w = 640
		self.h = 480

	def setup_window(self):
		'''Perform some magic (if needed) to set up a Gtk window'''
		if self.window_id:
			self.win = Gtk.Window(Gtk.WindowType.POPUP)

			gdk_display = GdkX11.X11Display.get_default()
			self.gdk_win = GdkX11.X11Window.foreign_new_for_display(gdk_display, self.window_id)

			# We show the window so we get a Gdk Window,
			# then we we can reparent it...
			self.win.show()
			self.win.get_window().reparent(self.gdk_win, 0, 0)

			x, y, w, h = self.gdk_win.get_geometry()

			# Make us cover our parent window
			self.win.move(0, 0)
			self.win.set_default_size(w, h)
			self.win.set_size_request(w, h)

			self.w, self.h = w, h
		else:
			self.win = Gtk.Window()
			self.win.set_default_size(self.w, self.h)

	def setup_browser(self):
		'''Sets up WebKit in our window'''
		self.browser = WebKit.WebView()
		self.browser.connect('load-changed', self.handle_login)
		self.browser.get_user_content_manager().add_script(WebKit.UserScript.new(
			PORTAL_LOGIN_SCRIPT,
			WebKit.UserContentInjectedFrames.TOP_FRAME,
			WebKit.UserScriptInjectionTime.END,
			None,
			None,
		))

		settings = self.browser.get_settings()

		# Try to enable webgl
		try:
			settings.set_enable_webgl(True)
		except Exception as err:
			print("Could not enable WebGL: {}".format(err))

		# Enable disk caching
		if self.disk_cache:
			context = self.browser.get_context()
			data_manager = context.get_website_data_manager()
			context.set_cache_model(WebKit.CacheModel.WEB_BROWSER)
			print("Cache directory:", data_manager.props.disk_cache_directory)

		# Take a stab at guessing whether we are running in the
		# XScreensaver preview window...
		if self.w < 320 and self.h < 240:
			self.browser.set_zoom_level(0.4)

		self.browser.set_size_request(self.w, self.h)

	def handle_login(self, view, event):
		if event != WebKit.LoadEvent.FINISHED or self.login_attempted:
			return
		uri = urlsplit(view.get_uri() or '')
		if uri.scheme != 'https' or uri.netloc != 'login.sma.energy':
			return
		# Never resubmit automatically after a rejected login or an MFA challenge.
		self.login_attempted = True
		try:
			credentials = load_credentials()
		except ValueError as error:
			print(error)
			return
		script = SMA_LOGIN_SCRIPT + '(' + json.dumps(credentials) + ');'
		view.run_javascript(script, None, self.handle_login_result)

	def handle_login_result(self, view, result):
		try:
			submitted = view.run_javascript_finish(result).get_js_value().to_boolean()
		except Exception:
			print('Automatischer SMA-Login konnte nicht bestätigt werden.')
			return
		if submitted:
			print('SMA-Anmeldeformular automatisch abgesendet.')
		else:
			print('Kein verwendbares SMA-Anmeldeformular gefunden; bitte im Fenster prüfen.')

	def setup_cookie_jar(self):
		COOKIE_FILE.parent.mkdir(mode=0o700, parents=True, exist_ok=True)
		COOKIE_FILE.touch(mode=0o600, exist_ok=True)
		COOKIE_FILE.chmod(0o600)
		context = self.browser.get_context()
		cookie_manager = context.get_cookie_manager()
		cookie_manager.set_accept_policy(WebKit.CookieAcceptPolicy.ALWAYS)
		cookie_manager.set_persistent_storage(str(COOKIE_FILE), WebKit.CookiePersistentStorage.TEXT)

	def setup_layout(self):
		'''Make sure the browser can expand without affecting the window'''
		sw = Gtk.Layout()
		sw.put(self.browser, 0, 0)
		self.win.add(sw)

	def setup(self):
		'''Do all the things!'''
		self.setup_window()
		self.setup_browser()
		self.setup_layout()
		self.setup_cookie_jar()

		def terminate(*args):
			Gtk.main_quit()

		self.win.connect('destroy', terminate)
		self.win.connect('delete-event', terminate)

		signal.signal(signal.SIGINT, signal.SIG_DFL)
		signal.signal(signal.SIGTERM, terminate)

		self.win.show_all()

		self.browser.load_uri(SUNNY_PORTAL_URL)

	@classmethod
	def determine_window_id(cls, win_id=None):
		'''Try and get an XID to use as our parent window'''
		if not win_id:
			win_id = os.getenv('XSCREENSAVER_WINDOW')

		if win_id:
			win_id = int(win_id, 16)

		return win_id


if __name__ == "__main__":
	import argparse

	parser = argparse.ArgumentParser(description='SMA-Screensaver: Sunny Portal screensaver (fork of WebScreensaver)')
	parser.add_argument('-window-id', '--window-id', help='XID of Window to draw on')
	parser.add_argument('-no-cache', action='store_true', help='Disable disk caching of website data')
	args = parser.parse_args()

	saver = WebScreensaver(
		window_id=WebScreensaver.determine_window_id(args.window_id),
		disk_cache=not args.no_cache
	)
	saver.setup()

	Gtk.main()
