#!/usr/bin/env python
# Safe Eyes is a utility to remind you to take break frequently
# to protect your eyes from eye strain.

# Copyright (C) 2017  Gobinath

# 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 3 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, see <http://www.gnu.org/licenses/>.
"""
SafeEyes connects all the individual components and provide the complete application.
"""

import atexit
import logging
import os
from threading import Timer

import gi
from safeeyes import utility
from safeeyes.ui.about_dialog import AboutDialog
from safeeyes.ui.break_screen import BreakScreen
from safeeyes.ui.required_plugin_dialog import RequiredPluginDialog
from safeeyes.model import State, RequiredPluginException
from safeeyes.rpc import RPCServer
from safeeyes.plugin_manager import PluginManager
from safeeyes.core import SafeEyesCore
from safeeyes.ui.settings_dialog import SettingsDialog

gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, Gio, GLib

SAFE_EYES_VERSION = "2.2.3"


class SafeEyes(Gtk.Application):
    """
    This class represents a runnable Safe Eyes instance.
    """

    required_plugin_dialog_active = False
    retry_errored_plugins_count = 0

    def __init__(self, system_locale, config, cli_args):
        super().__init__(
            application_id="io.github.slgobinath.SafeEyes",
            flags=Gio.ApplicationFlags.FLAGS_NONE ## This is necessary for compatibility with Ubuntu 22.04.
        )
        self.active = False
        self.break_screen = None
        self.safe_eyes_core = None
        self.config = config
        self.context = {}
        self.plugins_manager = None
        self.settings_dialog_active = False
        self.rpc_server = None
        self._status = ''
        self.cli_args = cli_args
        self.system_locale = system_locale

    def start(self):
        """
        Start Safe Eyes
        """
        self.run()

    def do_startup(self):
        Gtk.Application.do_startup(self)

        logging.info('Starting up Application')

        # Initialize the Safe Eyes Context
        self.context['version'] = SAFE_EYES_VERSION
        self.context['desktop'] = utility.desktop_environment()
        self.context['is_wayland'] = utility.is_wayland()
        self.context['locale'] = self.system_locale
        self.context['api'] = {}
        self.context['api']['show_settings'] = lambda: utility.execute_main_thread(
            self.show_settings)
        self.context['api']['show_about'] = lambda: utility.execute_main_thread(
            self.show_about)
        self.context['api']['enable_safeeyes'] = lambda next_break_time=-1, reset_breaks=False: \
            utility.execute_main_thread(self.enable_safeeyes, next_break_time, reset_breaks)
        self.context['api']['disable_safeeyes'] = lambda status=None, is_resting=False: utility.execute_main_thread(
            self.disable_safeeyes, status, is_resting)
        self.context['api']['status'] = self.status
        self.context['api']['quit'] = lambda: utility.execute_main_thread(
            self.quit)
        if self.config.get('persist_state'):
            self.context['session'] = utility.open_session()
        else:
            self.context['session'] = {'plugin': {}}

        self.break_screen = BreakScreen(
            self.context, self.on_skipped, self.on_postponed, utility.STYLE_SHEET_PATH)
        self.break_screen.initialize(self.config)
        self.plugins_manager = PluginManager()
        self.safe_eyes_core = SafeEyesCore(self.context)
        self.safe_eyes_core.on_pre_break += self.plugins_manager.pre_break
        self.safe_eyes_core.on_start_break += self.on_start_break
        self.safe_eyes_core.start_break += self.start_break
        self.safe_eyes_core.on_count_down += self.countdown
        self.safe_eyes_core.on_stop_break += self.stop_break
        self.safe_eyes_core.on_update_next_break += self.update_next_break
        self.safe_eyes_core.initialize(self.config)
        self.context['api']['take_break'] = self.take_break
        self.context['api']['has_breaks'] = self.safe_eyes_core.has_breaks
        self.context['api']['postpone'] = self.safe_eyes_core.postpone
        self.context['api']['get_break_time'] = self.safe_eyes_core.get_break_time

        try:
            self.plugins_manager.init(self.context, self.config)
        except RequiredPluginException as e:
            self.show_required_plugin_dialog(e)

        self.hold()

        atexit.register(self.persist_session)

        if self.config.get('use_rpc_server', True):
            self.__start_rpc_server()

        if not self.plugins_manager.needs_retry() and not self.required_plugin_dialog_active and self.safe_eyes_core.has_breaks():
            self.active = True
            self.context['state'] = State.START
            self.plugins_manager.start()		# Call the start method of all plugins
            self.safe_eyes_core.start()
            self.handle_system_suspend()

    def do_activate(self):
        logging.info('Application activated')

        if self.plugins_manager.needs_retry():
            GLib.timeout_add_seconds(1, self._retry_errored_plugins)

        if self.cli_args.about:
            self.show_about()
        elif self.cli_args.disable:
            self.disable_safeeyes()
        elif self.cli_args.enable:
            self.enable_safeeyes()
        elif self.cli_args.settings:
            self.show_settings()
        elif self.cli_args.take_break:
            self.take_break()


    def _retry_errored_plugins(self):
        if not self.plugins_manager.needs_retry():
            return

        logging.info(f"Retry loading errored plugin")
        self.plugins_manager.retry_errored_plugins()

        error = self.plugins_manager.get_retryable_error()

        if error is None:
            # success
            self.restart(self.config, set_active=True)
            return

        # errored again
        if self.retry_errored_plugins_count >= 3:
            self.show_required_plugin_dialog(error)
            return

        timeout = pow(2, self.retry_errored_plugins_count)
        self.retry_errored_plugins_count += 1

        GLib.timeout_add_seconds(timeout, self._retry_errored_plugins)


    def show_settings(self):
        """
        Listen to tray icon Settings action and send the signal to Settings dialog.
        """
        if not self.settings_dialog_active:
            logging.info("Show Settings dialog")
            self.settings_dialog_active = True
            settings_dialog = SettingsDialog(
                self.config.clone(), self.save_settings)
            settings_dialog.show()

    def show_required_plugin_dialog(self, error: RequiredPluginException):
        self.required_plugin_dialog_active = True

        logging.info("Show RequiredPlugin dialog")
        dialog = RequiredPluginDialog(
            error.get_plugin_id(),
            error.get_plugin_name(),
            error.get_message(),
            self.quit,
            lambda: self.disable_plugin(plugin_id)
        )
        dialog.show()

    def disable_plugin(self, plugin_id):
        """
        Temporarily disable plugin, and restart SafeEyes.
        """
        config = self.config.clone()

        for plugin in config.get('plugins'):
            if plugin['id'] == plugin_id:
                plugin['enabled'] = False

        self.required_plugin_dialog_active = False

        self.restart(config, set_active=True)

    def show_about(self):
        """
        Listen to tray icon About action and send the signal to About dialog.
        """
        logging.info("Show About dialog")
        about_dialog = AboutDialog(SAFE_EYES_VERSION)
        about_dialog.show()

    def quit(self):
        """
        Listen to the tray menu quit action and stop the core, notification and the app itself.
        """
        logging.info("Quit Safe Eyes")
        self.break_screen.close()
        self.context['state'] = State.QUIT
        self.plugins_manager.stop()
        self.safe_eyes_core.stop()
        self.plugins_manager.exit()
        self.__stop_rpc_server()
        self.persist_session()

        super().quit()

    def handle_suspend_callback(self, sleeping):
        """
        If the system goes to sleep, Safe Eyes stop the core if it is already active.
        If it was active, Safe Eyes will become active after wake up.
        """
        if sleeping:
            # Sleeping / suspending
            if self.active:
                logging.info("Stop Safe Eyes due to system suspend")
                self.plugins_manager.stop()
                self.safe_eyes_core.stop(True)
        else:
            # Resume from sleep
            if self.active and self.safe_eyes_core.has_breaks():
                logging.info("Resume Safe Eyes after system wakeup")
                self.plugins_manager.start()
                self.safe_eyes_core.start()

    def handle_suspend_signal(self, proxy, sender, signal, parameters):
        if signal != "PrepareForSleep":
            return

        (sleeping, ) = parameters

        self.handle_suspend_callback(sleeping)

    def handle_system_suspend(self):
        """
        Setup system suspend listener.
        """
        self.suspend_proxy = Gio.DBusProxy.new_for_bus_sync(
            bus_type=Gio.BusType.SYSTEM,
            flags=Gio.DBusProxyFlags.DO_NOT_LOAD_PROPERTIES,
            info=None,
            name='org.freedesktop.login1',
            object_path='/org/freedesktop/login1',
            interface_name='org.freedesktop.login1.Manager',
            cancellable=None,
        )
        self.suspend_proxy.connect('g-signal', self.handle_suspend_signal)

    def on_skipped(self):
        """
        Listen to break screen Skip action and send the signal to core.
        """
        logging.info("User skipped the break")
        self.safe_eyes_core.skip()
        self.plugins_manager.stop_break()

    def on_postponed(self):
        """
        Listen to break screen Postpone action and send the signal to core.
        """
        logging.info("User postponed the break")
        self.safe_eyes_core.postpone()
        self.plugins_manager.stop_break()

    def save_settings(self, config):
        """
        Listen to Settings dialog Save action and write to the config file.
        """
        self.settings_dialog_active = False

        if self.config == config:
            # Config is not modified
            return

        logging.info("Saving settings to safeeyes.json")
        # Stop the Safe Eyes core
        if self.active:
            self.plugins_manager.stop()
            self.safe_eyes_core.stop()

        # Write the configuration to file
        config.save()
        self.persist_session()

        self.restart(config)

    def restart(self, config, set_active=False):
        logging.info("Initialize SafeEyesCore with modified settings")

        if self.rpc_server is None and config.get('use_rpc_server'):
            # RPC server wasn't running but now enabled
            self.__start_rpc_server()
        elif self.rpc_server is not None and not config.get('use_rpc_server'):
            # RPC server was running but now disabled
            self.__stop_rpc_server()

        # Restart the core and initialize the components
        self.config = config
        self.safe_eyes_core.initialize(config)
        self.break_screen.initialize(config)

        try:
            self.plugins_manager.init(self.context, self.config)
        except RequiredPluginException as e:
            self.show_required_plugin_dialog(e)
            return

        if set_active:
            self.active = True

        if self.active and self.safe_eyes_core.has_breaks():
            # 1 sec delay is required to give enough time for core to be stopped
            Timer(1.0, self.safe_eyes_core.start).start()
            self.plugins_manager.start()

    def enable_safeeyes(self, scheduled_next_break_time=-1, reset_breaks=False):
        """
        Listen to tray icon enable action and send the signal to core.
        """
        if not self.required_plugin_dialog_active and not self.active and self.safe_eyes_core.has_breaks():
            self.active = True
            self.safe_eyes_core.start(scheduled_next_break_time, reset_breaks)
            self.plugins_manager.start()

    def disable_safeeyes(self, status=None, is_resting = False):
        """
        Listen to tray icon disable action and send the signal to core.
        """
        if self.active:
            self.active = False
            self.plugins_manager.stop()
            self.safe_eyes_core.stop(is_resting)
            if status is None:
                status = _('Disabled until restart')
            self._status = status

    def on_start_break(self, break_obj):
        """
        Pass the break information to plugins.
        """
        if not self.plugins_manager.start_break(break_obj):
            return False
        return True

    def start_break(self, break_obj):
        """
        Pass the break information to break screen.
        """
        # Get the HTML widgets content from plugins
        widget = self.plugins_manager.get_break_screen_widgets(break_obj)
        actions = self.plugins_manager.get_break_screen_tray_actions(break_obj)
        self.break_screen.show_message(break_obj, widget, actions)

    def countdown(self, countdown, seconds):
        """
        Pass the countdown to plugins and break screen.
        """
        self.break_screen.show_count_down(countdown, seconds)
        self.plugins_manager.countdown(countdown, seconds)
        return True

    def update_next_break(self, break_obj, break_time):
        """
        Update the next break to plugins and save the session.
        """
        self.plugins_manager.update_next_break(break_obj, break_time)
        self._status = _('Next break at %s') % (
            utility.format_time(break_time))
        if self.config.get('persist_state'):
            utility.write_json(utility.SESSION_FILE_PATH,
                               self.context['session'])

    def stop_break(self):
        """
        Stop the current break.
        """
        self.break_screen.close()
        self.plugins_manager.stop_break()
        return True

    def take_break(self, break_type = None):
        """
        Take a break now.
        """
        utility.execute_main_thread(self.safe_eyes_core.take_break, break_type)

    def status(self):
        """
        Return the status of Safe Eyes.
        """
        return self._status

    def persist_session(self):
        """
        Save the session object to the session file.
        """
        if self.config.get('persist_state'):
            utility.write_json(utility.SESSION_FILE_PATH,
                               self.context['session'])
        else:
            utility.delete(utility.SESSION_FILE_PATH)

    def __start_rpc_server(self):
        if self.rpc_server is None:
            self.rpc_server = RPCServer(self.config.get('rpc_port'), self.context)
            self.rpc_server.start()

    def __stop_rpc_server(self):
        if self.rpc_server is not None:
            self.rpc_server.stop()
            self.rpc_server = None
