#!/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/>.
"""
PluginManager loads all enabled plugins and call their lifecycle methods.
A plugin must have the following directory structure:
<plugin-id>
    |- config.json
    |- plugin.py
    |- icon.png (Optional)

The plugin.py can have following methods but all are optional:
 - description()
    If a custom description has to be displayed, use this function
 - init(context, safeeyes_config, plugin_config)
    Initialize the plugin. Will be called after loading and after every changes in configuration
 - on_start()
    Executes when Safe Eyes is enabled
 - on_stop()
    Executes when Safe Eyes is disabled
 - on_exit()
    Executes before Safe Eyes exits
 - on_pre_break(break_obj)
    Executes at the start of the prepare time for a break
 - on_start_break(break_obj)
    Executes when a break starts
 - on_stop_break()
    Executes when a break stops
 - on_countdown(countdown, seconds)
    Executes every second throughout a break
 - update_next_break(break_obj, break_time)
    Executes when the next break changes
 - enable()
    Executes once the plugin.py is loaded as a module
 - disable()
    Executes if the plugin is disabled at the runtime by the user
"""

import importlib
import logging
import os
import sys

from safeeyes import utility
from safeeyes.model import RequiredPluginException

sys.path.append(os.path.abspath(utility.SYSTEM_PLUGINS_DIR))
sys.path.append(os.path.abspath(utility.USER_PLUGINS_DIR))

HORIZONTAL_LINE_LENGTH = 64


class PluginManager:
    """
    Imports the Safe Eyes plugins and calls the methods defined in those plugins.
    """

    def __init__(self):
        logging.info('Load all the plugins')
        self.__plugins = {}
        self.__plugins_on_init = []
        self.__plugins_on_start = []
        self.__plugins_on_stop = []
        self.__plugins_on_exit = []
        self.__plugins_on_pre_break = []
        self.__plugins_on_start_break = []
        self.__plugins_on_stop_break = []
        self.__plugins_on_countdown = []
        self.__plugins_update_next_break = []
        self.__widget_plugins = []
        self.__tray_actions_plugins = []
        self.last_break = None
        self.horizontal_line = '─' * HORIZONTAL_LINE_LENGTH

    def init(self, context, config):
        """
        Initialize all the plugins with init(context, safe_eyes_config, plugin_config) function.
        """
        # Load the plugins
        for plugin in config.get('plugins'):
            try:
                self.__load_plugin(plugin)
            except RequiredPluginException as e:
                raise e
            except BaseException as e:
                traceback_wanted = logging.getLogger().getEffectiveLevel() == logging.DEBUG
                if traceback_wanted:
                    import traceback
                    traceback.print_exc()
                logging.error('Error in loading the plugin %s: %s', plugin['id'], e)
                continue
        # Initialize the plugins
        for plugin in self.__plugins_on_init:
            plugin['module'].init(context, config, plugin['config'])
        return True

    def start(self):
        """
        Execute the on_start() function of plugins.
        """
        for plugin in self.__plugins_on_start:
            plugin['module'].on_start()
        return True

    def stop(self):
        """
        Execute the on_stop() function of plugins.
        """
        for plugin in self.__plugins_on_stop:
            plugin['module'].on_stop()
        return True

    def exit(self):
        """
        Execute the on_exit() function of plugins.
        """
        for plugin in self.__plugins_on_exit:
            plugin['module'].on_exit()
        return True

    def pre_break(self, break_obj):
        """
        Execute the on_pre_break(break_obj) function of plugins.
        """
        for plugin in self.__plugins_on_pre_break:
            if break_obj.plugin_enabled(plugin['id'], plugin['enabled']):
                if plugin['module'].on_pre_break(break_obj):
                    return False
        return True

    def start_break(self, break_obj):
        """
        Execute the start_break(break_obj) function of plugins.
        """
        self.last_break = break_obj
        for plugin in self.__plugins_on_start_break:
            if break_obj.plugin_enabled(plugin['id'], plugin['enabled']):
                if plugin['module'].on_start_break(break_obj):
                    return False

        return True

    def stop_break(self):
        """
        Execute the stop_break() function of plugins.
        """
        for plugin in self.__plugins_on_stop_break:
            if self.last_break.plugin_enabled(plugin['id'], plugin['enabled']):
                plugin['module'].on_stop_break()

    def countdown(self, countdown, seconds):
        """
        Execute the on_countdown(countdown, seconds) function of plugins.
        """
        for plugin in self.__plugins_on_countdown:
            if self.last_break.plugin_enabled(plugin['id'], plugin['enabled']):
                plugin['module'].on_countdown(countdown, seconds)

    def update_next_break(self, break_obj, break_time):
        """
        Execute the update_next_break(break_time) function of plugins.
        """
        for plugin in self.__plugins_update_next_break:
            plugin['module'].update_next_break(break_obj, break_time)
        return True

    def get_break_screen_widgets(self, break_obj):
        """
        Return the HTML widget generated by the plugins.
        The widget is generated by calling the get_widget_title and get_widget_content functions of plugins.
        """
        widget = ''
        for plugin in self.__widget_plugins:
            if break_obj.plugin_enabled(plugin['id'], plugin['enabled']):
                try:
                    title = plugin['module'].get_widget_title(break_obj).upper().strip()
                    if title == '':
                        continue
                    content = plugin['module'].get_widget_content(break_obj)
                    if content == '':
                        continue
                    widget += '<b>{}</b>\n{}\n{}\n\n\n'.format(title, self.horizontal_line, content)
                except BaseException:
                    continue
        return widget.strip()

    def get_break_screen_tray_actions(self, break_obj):
        """
        Return Tray Actions.
        """
        actions = []
        for plugin in self.__tray_actions_plugins:
            if break_obj.plugin_enabled(plugin['id'], plugin['enabled']):
                action = plugin['module'].get_tray_action(break_obj)
                if action:
                    actions.append(action)

        return actions

    def __load_plugin(self, plugin):
        """
        Load the given plugin.
        """
        plugin_enabled = plugin['enabled']
        if plugin['id'] in self.__plugins and not plugin_enabled:
            # A disabled plugin but that was loaded earlier
            plugin_obj = self.__plugins[plugin['id']]
            if plugin_obj['enabled']:
                # Previously enabled but now disabled
                plugin_obj['enabled'] = False
                utility.remove_if_exists(self.__plugins_on_start, plugin_obj)
                utility.remove_if_exists(self.__plugins_on_stop, plugin_obj)
                utility.remove_if_exists(self.__plugins_on_exit, plugin_obj)
                utility.remove_if_exists(self.__plugins_update_next_break, plugin_obj)
                # Call the plugin.disable method if available
                if utility.has_method(plugin_obj['module'], 'disable'):
                    plugin_obj['module'].disable()
                logging.info("Successfully unloaded the plugin '%s'", plugin['id'])

            if not plugin_obj['break_override_allowed']:
                # Remaining methods also should be removed
                utility.remove_if_exists(self.__plugins_on_init, plugin_obj)
                utility.remove_if_exists(self.__plugins_on_pre_break, plugin_obj)
                utility.remove_if_exists(self.__plugins_on_start_break, plugin_obj)
                utility.remove_if_exists(self.__plugins_on_stop_break, plugin_obj)
                utility.remove_if_exists(self.__plugins_on_countdown, plugin_obj)
                utility.remove_if_exists(self.__widget_plugins, plugin_obj)
                utility.remove_if_exists(self.__tray_actions_plugins, plugin_obj)
                del self.__plugins[plugin['id']]
            return

        # Look for plugin.py
        if os.path.isfile(os.path.join(utility.SYSTEM_PLUGINS_DIR, plugin['id'], 'plugin.py')):
            plugin_dir = utility.SYSTEM_PLUGINS_DIR
        elif os.path.isfile(os.path.join(utility.USER_PLUGINS_DIR, plugin['id'], 'plugin.py')):
            plugin_dir = utility.USER_PLUGINS_DIR
        else:
            logging.error('plugin.py not found for the plugin: %s', plugin['id'])
            return
        # Look for config.json
        plugin_path = os.path.join(plugin_dir, plugin['id'])
        plugin_config_path = os.path.join(plugin_path, 'config.json')
        if not os.path.isfile(plugin_config_path):
            logging.error('config.json not found for the plugin: %s', plugin['id'])
            return
        plugin_config = utility.load_json(plugin_config_path)
        if plugin_config is None:
            return

        if plugin_enabled or plugin_config.get('break_override_allowed', False):
            if plugin['id'] in self.__plugins:
                # The plugin is already enabled or partially loaded due to break_override_allowed

                # Validate the dependencies again
                if utility.check_plugin_dependencies(plugin['id'], plugin_config, plugin.get('settings', {}), plugin_path):
                    plugin_obj['enabled'] = False
                    utility.remove_if_exists(self.__plugins_on_start, plugin_obj)
                    utility.remove_if_exists(self.__plugins_on_stop, plugin_obj)
                    utility.remove_if_exists(self.__plugins_on_exit, plugin_obj)
                    utility.remove_if_exists(self.__plugins_update_next_break, plugin_obj)
                    utility.remove_if_exists(self.__plugins_on_init, plugin_obj)
                    utility.remove_if_exists(self.__plugins_on_pre_break, plugin_obj)
                    utility.remove_if_exists(self.__plugins_on_start_break, plugin_obj)
                    utility.remove_if_exists(self.__plugins_on_stop_break, plugin_obj)
                    utility.remove_if_exists(self.__plugins_on_countdown, plugin_obj)
                    utility.remove_if_exists(self.__widget_plugins, plugin_obj)
                    utility.remove_if_exists(self.__tray_actions_plugins, plugin_obj)
                    del self.__plugins[plugin['id']]

                # Use the existing plugin object
                plugin_obj = self.__plugins[plugin['id']]

                # Update the config
                plugin_obj['config'] = dict(plugin.get('settings', {}))
                plugin_obj['config']['path'] = os.path.join(plugin_dir, plugin['id'])

                if plugin_obj['enabled']:
                    # Already loaded completely
                    return
                # Plugin was partially loaded due to break_override_allowed
                if plugin_enabled:
                    # Load the rest of the methods
                    plugin_obj['enabled'] = True
                    module = plugin_obj['module']
                    self.__init_plugin(module, plugin_obj)
            else:
                # This is the first time to load the plugin
                # Check for dependencies
                message = utility.check_plugin_dependencies(plugin['id'], plugin_config, plugin.get('settings', {}), plugin_path)
                if message:
                    if plugin_config.get('required_plugin', False):
                        raise RequiredPluginException(
                            plugin['id'],
                            plugin_config['meta']['name'],
                            message
                        )
                    return

                # Load the plugin module
                module = importlib.import_module((plugin['id'] + '.plugin'))
                logging.info("Successfully loaded %s", str(module))
                plugin_obj = {'id': plugin['id'], 'module': module, 'config': dict(plugin.get(
                    'settings', {})), 'enabled': plugin_enabled,
                              'break_override_allowed': plugin_config.get('break_override_allowed', False)}
                # Inject the plugin directory into the config
                plugin_obj['config']['path'] = os.path.join(plugin_dir, plugin['id'])
                self.__plugins[plugin['id']] = plugin_obj
                if utility.has_method(module, 'enable'):
                    module.enable()
                if plugin_enabled:
                    self.__init_plugin(module, plugin_obj)
                if utility.has_method(module, 'init', 3):
                    self.__plugins_on_init.append(plugin_obj)
                if utility.has_method(module, 'on_pre_break', 1):
                    self.__plugins_on_pre_break.append(plugin_obj)
                if utility.has_method(module, 'on_start_break', 1):
                    self.__plugins_on_start_break.append(plugin_obj)
                if utility.has_method(module, 'on_stop_break', 0):
                    self.__plugins_on_stop_break.append(plugin_obj)
                if utility.has_method(module, 'on_countdown', 2):
                    self.__plugins_on_countdown.append(plugin_obj)
                if utility.has_method(module, 'get_widget_title', 1) and utility.has_method(module,
                                                                                            'get_widget_content', 1):
                    self.__widget_plugins.append(plugin_obj)
                if utility.has_method(module, 'get_tray_action', 1):
                    self.__tray_actions_plugins.append(plugin_obj)

    def __init_plugin(self, module, plugin_obj):
        """
        Collect mandatory methods from the plugin and add them to the life cycle methods list.
        """
        if utility.has_method(module, 'on_start'):
            self.__plugins_on_start.append(plugin_obj)
        if utility.has_method(module, 'on_stop'):
            self.__plugins_on_stop.append(plugin_obj)
        if utility.has_method(module, 'on_exit'):
            self.__plugins_on_exit.append(plugin_obj)
        if utility.has_method(module, 'update_next_break', 2):
            self.__plugins_update_next_break.append(plugin_obj)
