# -*- coding: utf-8 -*-
"""
kodiswift.cli.app
----------------

This package contains the code which runs plugins from the command line.

:copyright: (c) 2012 by Jonathan Beluch
:license: GPLv3, see LICENSE for more details.
"""
from __future__ import absolute_import

import logging
import os
import sys
from xml.etree import ElementTree as Et

from kodiswift import Plugin, ListItem, logger
from kodiswift.cli import Option
from kodiswift.cli.console import (display_listitems, continue_or_quit,
                                   get_user_choice)
from kodiswift.common import Modes

__all__ = ['get_addon_module_name', 'crawl', 'RunCommand', 'PluginManager',
           'setup_options', 'patch_sysargv', 'patch_plugin', 'once',
           'interactive']


class RunCommand(object):
    """A CLI command to run a plugin."""

    command = 'run'
    usage = '%prog run [once|interactive|crawl] [url]'
    option_list = (
        Option('-q', '--quiet', action='store_true',
               help='set logging level to quiet'),
        Option('-v', '--verbose', action='store_true',
               help='set logging level to verbose'),
    )

    @staticmethod
    def run(opts, args):
        """The run method for the 'run' command. Executes a plugin from the
        command line.
        """
        setup_options(opts)

        mode = Modes.ONCE
        if len(args) > 0 and hasattr(Modes, args[0].upper()):
            _mode = args.pop(0).upper()
            mode = getattr(Modes, _mode)

        url = None
        if len(args) > 0:
            # A url was specified
            url = args.pop(0)

        plugin_mgr = PluginManager.load_plugin_from_addon_xml(mode, url)
        plugin_mgr.run()


def setup_options(opts):
    """Takes any actions necessary based on command line options"""
    if opts.quiet:
        logger.log.setLevel(logging.WARNING)
        logger.GLOBAL_LOG_LEVEL = logging.WARNING

    if opts.verbose:
        logger.log.setLevel(logging.DEBUG)
        logger.GLOBAL_LOG_LEVEL = logging.DEBUG


def get_addon_module_name(addon_xml_filename):
    """Attempts to extract a module name for the given addon's addon.xml file.
    Looks for the 'xbmc.python.pluginsource' extension node and returns the
    addon's filename without the .py suffix.
    """
    try:
        xml = Et.parse(addon_xml_filename).getroot()
    except IOError:
        sys.exit('Cannot find an addon.xml file in the current working '
                 'directory. Please run this command from the root directory '
                 'of an addon.')

    try:
        plugin_source = (ext for ext in xml.findall('extension') if
                         ext.get('point') == 'xbmc.python.pluginsource').next()
    except StopIteration:
        sys.exit('ERROR, no pluginsource in addonxml')

    return plugin_source.get('library').split('.')[0]


class PluginManager(object):
    """A class to handle running a plugin in CLI mode. Handles setup state
    before calling plugin.run().
    """

    @classmethod
    def load_plugin_from_addon_xml(cls, mode, url):
        """Attempts to import a plugin's source code and find an instance of
        :class:`~kodiswift.Plugin`. Returns an instance of PluginManager if
        successful.
        """
        cwd = os.getcwd()
        sys.path.insert(0, cwd)
        module_name = get_addon_module_name(os.path.join(cwd, 'addon.xml'))
        addon = __import__(module_name)

        # Find the first instance of kodiswift.Plugin
        try:
            plugin = (attr_value for attr_value in vars(addon).values()
                      if isinstance(attr_value, Plugin)).next()
        except StopIteration:
            sys.exit('Could not find a Plugin instance in %s.py' % module_name)

        return cls(plugin, mode, url)

    def __init__(self, plugin, mode, url):
        self.plugin = plugin
        self.mode = mode
        self.url = url

    def run(self):
        """This method runs the the plugin in the appropriate mode parsed from
        the command line options.
        """
        handle = 0
        handlers = {
            Modes.ONCE: once,
            Modes.CRAWL: crawl,
            Modes.INTERACTIVE: interactive,
        }
        handler = handlers[self.mode]
        patch_sysargv(self.url or 'plugin://%s/' % self.plugin.id, handle)
        return handler(self.plugin)


def patch_sysargv(*args):
    """Patches sys.argv with the provided args"""
    sys.argv = args[:]


def patch_plugin(plugin, path, handle=None):
    """Patches a few attributes of a plugin instance to enable a new call to
    plugin.run()
    """
    if handle is None:
        handle = plugin.request.handle
    patch_sysargv(path, handle)
    plugin._end_of_directory = False


def once(plugin, parent_stack=None):
    """A run mode for the CLI that runs the plugin once and exits."""
    plugin.clear_added_items()
    items = plugin.run()

    # if update_listing=True, we need to remove the last url from the parent
    # stack
    if parent_stack and plugin._update_listing:
        del parent_stack[-1]

    # if we have parent items, include the most recent in the display
    if parent_stack:
        items.insert(0, parent_stack[-1])

    display_listitems(items, plugin.request.url)
    return items


def interactive(plugin):
    """A run mode for the CLI that runs the plugin in a loop based on user
    input.
    """
    items = [item for item in once(plugin) if not item.get_played()]
    parent_stack = []  # Keep track of parents so we can have a '..' option

    selected_item = get_user_choice(items)
    while selected_item is not None:
        if parent_stack and selected_item == parent_stack[-1]:
            # User selected the parent item, remove from list
            parent_stack.pop()
        else:
            # User selected non parent item, add current url to parent stack
            parent_stack.append(ListItem.from_dict(label='..',
                                                   path=plugin.request.url))
        patch_plugin(plugin, selected_item.get_path())

        items = [item for item in once(plugin, parent_stack=parent_stack)
                 if not item.get_played()]
        selected_item = get_user_choice(items)


def crawl(plugin):
    """Performs a breadth-first crawl of all possible routes from the
    starting path. Will only visit a URL once, even if it is referenced
    multiple times in a plugin. Requires user interaction in between each
    fetch.
    """
    # TODO: use OrderedSet?
    paths_visited = set()
    paths_to_visit = set(item.get_path() for item in once(plugin))

    while paths_to_visit and continue_or_quit():
        path = paths_to_visit.pop()
        paths_visited.add(path)

        # Run the new listitem
        patch_plugin(plugin, path)
        new_paths = set(item.get_path() for item in once(plugin))

        # Filter new items by checking against urls_visited and
        # urls_tovisit
        paths_to_visit.update(path for path in new_paths
                              if path not in paths_visited)
