import sys
import threading
from collections import OrderedDict
import logging
import time
import json
import os
from i3situation.core import plugin_manager
from i3situation.core import config


def setup_file_logger(filename, formatting, log_level):
    """
    A helper function for creating a file logger.
    Accepts arguments, as it is used in Status and LoggingWriter.
    """
    logger = logging.getLogger()
    # If a stream handler has been attached, remove it.
    if logger.handlers:
        logger.removeHandler(logger.handlers[0])
    handler = logging.FileHandler(filename)
    logger.addHandler(handler)
    formatter = logging.Formatter(*formatting)
    handler.setFormatter(formatter)
    logger.setLevel(log_level)
    handler.setLevel(log_level)
    return logger


class Status():
    """
    Handles the running of the status utility and acts as the glue for the
    application.
    """
    def __init__(self):
        self.config = config.Config(self.discover_folder_path())
        self.output_dict = OrderedDict()
        self._config_file_path = self.config.config_file_path
        self._plugin_path = self.config.plugin_path
        self._config_mod_time = os.path.getmtime(self._config_file_path)
        self._plugin_mod_time = os.path.getmtime(self._plugin_path)
        log_formatting = ('[%(asctime)s] - %(levelname)s - %(filename)s - '
                '%(funcName)s - %(message)s', '%d/%m/%Y %I:%M:%S %p')
        self.logger = setup_file_logger(self.config.general['log_file'],
                log_formatting, self.config.general['logging_level'])

        # Redirect stderr so that it doesn't confuse i3bar by outputting to it.
        self.log_writer = self.LoggingWriter(
                self.config.general['log_file'], self.config.general['logging_level'])
        sys.stderr = self.log_writer
        logging.debug('Config loaded from {0}'.format(self._config_file_path))
        logging.debug('Plugin path is located at {0}'.format(self._plugin_path))
        logging.debug('Last config modification time is: {0}'.format(self._config_mod_time))
        logging.debug('Last plugin directory modification time is: {0}'.format(self._plugin_mod_time))
        self.output_to_bar(json.dumps({'version': 1, 'click_events': True}), False)
        self.output_to_bar('[', False)
        logging.debug('Sent initial JSON data to i3bar.')
        logging.debug('Beginning plugin loading process')
        self.loader = plugin_manager.PluginLoader(
            self._plugin_path, self.config.plugin)
        self.thread_manager = plugin_manager.ThreadManager(self.output_dict)
        # Event handling is done in another thread, so that the main thread
        # isn't stalled.
        self.event_thread = threading.Thread(target=self.handle_events)
        self.event_thread.start()

    def discover_folder_path(self):
        if os.environ.get('$XDG_CONFIG_HOME', None):
            folder_locations = [os.path.join(os.environ.get('$XDG_CONFIG_HOME'),
                'i3situation'), '/etc/i3situation']
        else:
            folder_locations = [os.path.join(os.path.expanduser('~'), '.config',
                'i3situation'), '/etc/i3situation']
        return folder_locations

    def output_to_bar(self, message, comma=True):
        """
        Outputs data to stdout, without buffering.

        message: A string containing the data to be output.
        comma: Whether or not a comma should be placed at the end of the output.
        """
        if comma:
            message += ','
        sys.stdout.write(message + '\n')
        sys.stdout.flush()

    def reload(self):
        """
        Reload the installed plugins and the configuration file. This is called
        when either the plugins or config get updated.
        """
        logging.debug('Reloading config file as files have been modified.')
        self.config.plugin, self.config.general = self.config.reload()
        logging.debug('Reloading plugins as files have been modified.')
        self.loader = plugin_manager.PluginLoader(
            self._plugin_path, self.config.plugin)
        self._plugin_mod_time = os.path.getmtime(self._plugin_path)
        self._config_mod_time = os.path.getmtime(self._config_file_path)

    def run_plugins(self):
        """
        Creates a thread for each plugin and lets the thread_manager handle it.
        """
        for obj in self.loader.objects:
            # Reserve a slot in the output_dict in order to ensure that the
            # items are in the correct order.
            self.output_dict[obj.output_options['name']] = None
            self.thread_manager.add_thread(obj.main, obj.options['interval'])

    def run(self):
        """
        Monitors if the config file or plugins are updated. Also outputs the
        JSON data generated by the plugins, without needing to poll the threads.
        """
        self.run_plugins()
        while True:
            # Reload plugins and config if either the config file or plugin
            # directory are modified.
            if self._config_mod_time != os.path.getmtime(self._config_file_path) or \
                    self._plugin_mod_time != os.path.getmtime(self._plugin_path):
                self.thread_manager.kill_all_threads()
                self.output_dict.clear()
                self.reload()
                self.run_plugins()
            self.output_to_bar(json.dumps(self._remove_empty_output()))
            time.sleep(self.config.general['interval'])

    def _remove_empty_output(self):
        """
        If plugins haven't been initialised and therefore not sending output or
        their output is None, there is no reason to take up extra room on the
        bar.
        """
        output = []
        for key in self.output_dict:
            if self.output_dict[key] is not None and 'full_text' in self.output_dict[key]:
                output.append(self.output_dict[key])
        return output

    def handle_events(self):
        """
        An event handler that processes events from stdin and calls the on_click
        function of the respective object. This function is run in another
        thread, so as to not stall the main thread.
        """
        for event in sys.stdin:
            if event.startswith('['):
                continue
            name = json.loads(event.lstrip(','))['name']
            for obj in self.loader.objects:
                if obj.output_options['name'] == name:
                    obj.on_click(json.loads(event.lstrip(',')))

    class LoggingWriter():
        """
        A simple class that provides a file like interface to the logging
        utility. Allows stderr to be redirected to logging.
        """
        def __init__(self, filename, logging_level):
            # FIXME: This formatting string overwrites the one defined
            # before, has been mentioned on the internet as being a bug
            # in th elogging module, will investigate later.
            formatting = ('[%(asctime)s] - %(levelname)s - %(filename)s -'
                            ' %(threadName)s - %(funcName)s - %(message)s',
                            '%d/%m/%Y %I:%M:%S %p')
            self.logger = setup_file_logger(filename, formatting, logging_level)
            self.logging_level = logging_level

        def write(self, message):
            """
            Called once stderr is replaced and data needs to be written.
            """
            if message != '\n':
                self.logger.log(self.logging_level, message)
