"""
Plugin hooks for EDMC - Ian Norton, Jonathan Harris
"""
import os
import imp
import sys
import operator
import threading	# We don't use it, but plugins might
from traceback import print_exc

import Tkinter as tk
import myNotebook as nb

from config import config


# Dashboard Flags constants
FlagsDocked = 1<<0		# on a landing pad
FlagsLanded = 1<<1		# on planet surface
FlagsLandingGearDown = 1<<2
FlagsShieldsUp = 1<<3
FlagsSupercruise = 1<<4
FlagsFlightAssistOff = 1<<5
FlagsHardpointsDeployed = 1<<6
FlagsInWing = 1<<7
FlagsLightsOn = 1<<8
FlagsCargoScoopDeployed = 1<<9
FlagsSilentRunning = 1<<10
FlagsScoopingFuel = 1<<11
FlagsSrvHandbrake = 1<<12
FlagsSrvTurret = 1<<13		# using turret view
FlagsSrvUnderShip = 1<<14	# turret retracted
FlagsSrvDriveAssist = 1<<15
FlagsFsdMassLocked = 1<<16
FlagsFsdCharging = 1<<17
FlagsFsdCooldown = 1<<18
FlagsLowFuel = 1<<19		# <25%
FlagsOverHeating = 1<<20	# > 100%
FlagsHasLatLong = 1<<21
FlagsIsInDanger = 1<<22
FlagsBeingInterdicted = 1<<23
FlagsInMainShip = 1<<24
FlagsInFighter = 1<<25
FlagsInSRV = 1<<26
FlagsAnalysisMode = 1<<27	# Hud in Analysis mode
FlagsNightVision = 1<<28
FlagsAverageAltitude = 1<<29		# Altitude from Average radius
FlagsFsdJump = 1<<30
FlagsSrvHighBeam = 1<<31

# Dashboard GuiFocus constants
GuiFocusNoFocus = 0
GuiFocusInternalPanel = 1	# right hand side
GuiFocusExternalPanel = 2	# left hand side
GuiFocusCommsPanel = 3		# top
GuiFocusRolePanel = 4		# bottom
GuiFocusStationServices = 5
GuiFocusGalaxyMap = 6
GuiFocusSystemMap = 7
GuiFocusOrrery = 8
GuiFocusFSS = 9
GuiFocusSAA = 10
GuiFocusCodex = 11


# List of loaded Plugins
PLUGINS = []
PLUGINS_not_py3 = []

# For asynchronous error display
last_error = {
    'msg':  None,
    'root': None,
}


class Plugin(object):

    def __init__(self, name, loadfile):
        """
        Load a single plugin
        :param name: module name
        :param loadfile: the main .py file
        :raises Exception: Typically ImportError or OSError
        """

        self.name = name	# Display name.
        self.folder = name	# basename of plugin folder. None for internal plugins.
        self.module = None	# None for disabled plugins.

        if loadfile:
            sys.stdout.write(('loading plugin %s from "%s"\n' % (name.replace('.', '_'), loadfile)).encode('utf-8'))
            with open(loadfile, 'rb') as plugfile:
                module = imp.load_module('plugin_%s' % name.encode('ascii', 'replace').replace('.', '_'), plugfile, loadfile.encode(sys.getfilesystemencoding()),
                                         ('.py', 'r', imp.PY_SOURCE))
                if module.plugin_start.func_code.co_argcount == 0:
                    newname = module.plugin_start()
                else:
                    newname = module.plugin_start(os.path.dirname(loadfile))
                self.name = newname and unicode(newname) or name
                self.module = module
        else:
            sys.stdout.write('plugin %s disabled\n' % name)

    def _get_func(self, funcname):
        """
        Get a function from a plugin
        :param funcname:
        :returns: The function, or None if it isn't implemented.
        """
        return getattr(self.module, funcname, None)

    def get_app(self, parent):
        """
        If the plugin provides mainwindow content create and return it.
        :param parent: the parent frame for this entry.
        :returns: None, a tk Widget, or a pair of tk.Widgets
        """
        plugin_app = self._get_func('plugin_app')
        if plugin_app:
            try:
                appitem = plugin_app(parent)
                if appitem is None:
                    return None
                elif isinstance(appitem, tuple):
                    if len(appitem) != 2 or not isinstance(appitem[0], tk.Widget) or not isinstance(appitem[1], tk.Widget):
                        raise AssertionError
                elif not isinstance(appitem, tk.Widget):
                    raise AssertionError
                return appitem
            except:
                print_exc()
        return None

    def get_prefs(self, parent, cmdr, is_beta):
        """
        If the plugin provides a prefs frame, create and return it.
        :param parent: the parent frame for this preference tab.
        :param cmdr: current Cmdr name (or None). Relevant if you want to have
           different settings for different user accounts.
        :param is_beta: whether the player is in a Beta universe.
        :returns: a myNotebook Frame
        """
        plugin_prefs = self._get_func('plugin_prefs')
        if plugin_prefs:
            try:
                if plugin_prefs.func_code.co_argcount == 1:
                    frame = plugin_prefs(parent)
                else:
                    frame = plugin_prefs(parent, cmdr, is_beta)
                if not isinstance(frame, nb.Frame):
                    raise AssertionError
                return frame
            except:
                print_exc()
        return None


def load_plugins(master):
    """
    Find and load all plugins
    """
    last_error['root'] = master

    imp.acquire_lock()

    internal = []
    for name in os.listdir(config.internal_plugin_dir):
        if name.endswith('.py') and not name[0] in ['.', '_']:
            try:
                plugin = Plugin(name[:-3], os.path.join(config.internal_plugin_dir, name))
                plugin.folder = None	# Suppress listing in Plugins prefs tab
                internal.append(plugin)
            except:
                print_exc()
    PLUGINS.extend(sorted(internal, key = lambda p: operator.attrgetter('name')(p).lower()))

    # Add plugin folder to load path so packages can be loaded from plugin folder
    sys.path.append(config.plugin_dir)

    found = []
    # Load any plugins that are also packages first
    for name in sorted(os.listdir(config.plugin_dir),
                       key = lambda n: (not os.path.isfile(os.path.join(config.plugin_dir, n, '__init__.py')), n.lower())):
        if not os.path.isdir(os.path.join(config.plugin_dir, name)) or name[0] in ['.', '_']:
            pass
        elif name.endswith('.disabled'):
            name, discard = name.rsplit('.', 1)
            found.append(Plugin(name, None))
        else:
            try:
                # Add plugin's folder to load path in case plugin has internal package dependencies
                sys.path.append(os.path.join(config.plugin_dir, name))
                found.append(Plugin(name, os.path.join(config.plugin_dir, name, 'load.py')))
            except:
                print_exc()
    PLUGINS.extend(sorted(found, key = lambda p: operator.attrgetter('name')(p).lower()))

    #########################################################
    # Detect plugins that aren't yet ready for Python 3.x
    #########################################################
    for p in PLUGINS:
        if p.module and p.folder and not p._get_func('plugin_start3'):
            PLUGINS_not_py3.append(p)
    #########################################################

    imp.release_lock()

def provides(fn_name):
    """
    Find plugins that provide a function
    :param fn_name:
    :returns: list of names of plugins that provide this function
    .. versionadded:: 3.0.2
    """
    return [p.name for p in PLUGINS if p._get_func(fn_name)]

def invoke(plugin_name, fallback, fn_name, *args):
    """
    Invoke a function on a named plugin
    :param plugin_name: preferred plugin on which to invoke the function
    :param fallback: fallback plugin on which to invoke the function, or None
    :param fn_name:
    :param *args: arguments passed to the function
    :returns: return value from the function, or None if the function was not found
    .. versionadded:: 3.0.2
    """
    for plugin in PLUGINS:
        if plugin.name == plugin_name and plugin._get_func(fn_name):
            return plugin._get_func(fn_name)(*args)
    for plugin in PLUGINS:
        if plugin.name == fallback:
            assert plugin._get_func(fn_name), plugin.name	# fallback plugin should provide the function
            return plugin._get_func(fn_name)(*args)


def notify_stop():
    """
    Notify each plugin that the program is closing.
    If your plugin uses threads then stop and join() them before returning.
    .. versionadded:: 2.3.7
    """
    error = None
    for plugin in PLUGINS:
        plugin_stop = plugin._get_func('plugin_stop')
        if plugin_stop:
            try:
                newerror = plugin_stop()
                error = error or newerror
            except:
                print_exc()
    return error


def notify_prefs_cmdr_changed(cmdr, is_beta):
    """
    Notify each plugin that the Cmdr has been changed while the settings dialog is open.
    Relevant if you want to have different settings for different user accounts.
    :param cmdr: current Cmdr name (or None).
    :param is_beta: whether the player is in a Beta universe.
    """
    for plugin in PLUGINS:
        prefs_cmdr_changed = plugin._get_func('prefs_cmdr_changed')
        if prefs_cmdr_changed:
            try:
                prefs_cmdr_changed(cmdr, is_beta)
            except:
                print_exc()


def notify_prefs_changed(cmdr, is_beta):
    """
    Notify each plugin that the settings dialog has been closed.
    The prefs frame and any widgets you created in your `get_prefs()` callback
    will be destroyed on return from this function, so take a copy of any
    values that you want to save.
    :param cmdr: current Cmdr name (or None).
    :param is_beta: whether the player is in a Beta universe.
    """
    for plugin in PLUGINS:
        prefs_changed = plugin._get_func('prefs_changed')
        if prefs_changed:
            try:
                if prefs_changed.func_code.co_argcount == 0:
                    prefs_changed()
                else:
                    prefs_changed(cmdr, is_beta)
            except:
                print_exc()


def notify_journal_entry(cmdr, is_beta, system, station, entry, state):
    """
    Send a journal entry to each plugin.
    :param cmdr: The Cmdr name, or None if not yet known
    :param system: The current system, or None if not yet known
    :param station: The current station, or None if not docked or not yet known
    :param entry: The journal entry as a dictionary
    :param state: A dictionary containing info about the Cmdr, current ship and cargo
    :param is_beta: whether the player is in a Beta universe.
    :returns: Error message from the first plugin that returns one (if any)
    """
    error = None
    for plugin in PLUGINS:
        journal_entry = plugin._get_func('journal_entry')
        if journal_entry:
            try:
                # Pass a copy of the journal entry in case the callee modifies it
                if journal_entry.func_code.co_argcount == 4:
                    newerror = journal_entry(cmdr, system, station, dict(entry))
                elif journal_entry.func_code.co_argcount == 5:
                    newerror = journal_entry(cmdr, system, station, dict(entry), dict(state))
                else:
                    newerror = journal_entry(cmdr, is_beta, system, station, dict(entry), dict(state))
                error = error or newerror
            except:
                print_exc()
    return error


def notify_dashboard_entry(cmdr, is_beta, entry):
    """
    Send a status entry to each plugin.
    :param cmdr: The piloting Cmdr name
    :param is_beta: whether the player is in a Beta universe.
    :param entry: The status entry as a dictionary
    :returns: Error message from the first plugin that returns one (if any)
    """
    error = None
    for plugin in PLUGINS:
        status = plugin._get_func('dashboard_entry')
        if status:
            try:
                # Pass a copy of the status entry in case the callee modifies it
                newerror = status(cmdr, is_beta, dict(entry))
                error = error or newerror
            except:
                print_exc()
    return error


def notify_system_changed(timestamp, system, coordinates):
    """
    Send notification data to each plugin when we arrive at a new system.
    :param timestamp:
    :param system:
    .. deprecated:: 2.2
    Use :func:`journal_entry` with the 'FSDJump' event.
    """
    for plugin in PLUGINS:
        system_changed = plugin._get_func('system_changed')
        if system_changed:
            try:
                if system_changed.func_code.co_argcount == 2:
                    system_changed(timestamp, system)
                else:
                    system_changed(timestamp, system, coordinates)
            except:
                print_exc()


def notify_newdata(data, is_beta):
    """
    Send the latest EDMC data from the FD servers to each plugin
    :param data:
    :param is_beta: whether the player is in a Beta universe.
    :returns: Error message from the first plugin that returns one (if any)
    """
    error = None
    for plugin in PLUGINS:
        cmdr_data = plugin._get_func('cmdr_data')
        if cmdr_data:
            try:
                if cmdr_data.func_code.co_argcount == 1:
                    newerror = cmdr_data(data)
                else:
                    newerror = cmdr_data(data, is_beta)
                error = error or newerror
            except:
                print_exc()
    return error


def show_error(err):
    """
    Display an error message in the status line of the main window.
    :param err:
    .. versionadded:: 2.3.7
    """
    if err and last_error['root']:
        last_error['msg'] = unicode(err)
        last_error['root'].event_generate('<<PluginError>>', when="tail")