#!/usr/bin/env python3
# Coin Price Indicator
#
# Nil Gradisnik <nil.gradisnik@gmail.com>
# Sander Van de Moortel <sander.vandemoortel@gmail.com>
#


import signal
import yaml
import logging
import glob
import dbus
import importlib
import notify2
import gi
import os

os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide"

from indicator import Indicator
from about import AboutWindow
from plugin_selection import PluginSelectionWindow
from downloader import DownloadService, AsyncDownloadService

from os.path import abspath, dirname, isfile, basename
from dbus.mainloop.glib import DBusGMainLoop
from gi.repository import Gtk

try:
    from gi.repository import AppIndicator3 as AppIndicator
except ImportError:
    from gi.repository import AppIndicator


PROJECT_ROOT = abspath(dirname(dirname(__file__)))
SETTINGS_FILE = PROJECT_ROOT + '/user.conf'
if isfile("./LOGLEVEL"):
    with open("LOGLEVEL", "r") as f: logging.basicConfig(level=int(f.read()))

class Coin():
    config = yaml.load(open(PROJECT_ROOT + '/config.yaml', 'r'), Loader=yaml.SafeLoader)
    config['project_root'] = PROJECT_ROOT

    def __init__(self):
        self.downloader = AsyncDownloadService()
        self.unique_id = 0
        self.assets = {}

        self._load_exchanges()
        self._load_assets()
        self._load_settings()
        self._start_main()

        self.instances = []
        self.discoveries = 0
        self._add_many_indicators(self.settings.get('tickers'))
        self._start_gui()


    # Load exchange 'plug-ins' from exchanges dir
    def _load_exchanges(self):
        dirfiles = glob.glob(dirname(__file__) + "/exchanges/*.py")
        plugins = [basename(f)[:-3] for f in dirfiles if isfile(f) and not f.endswith('__init__.py')]
        plugins.sort()

        self.EXCHANGES = []
        for plugin in plugins:
            class_name = plugin.capitalize()
            class_ = getattr(importlib.import_module('exchanges.' + plugin), class_name)
            self.EXCHANGES.append(class_)

    # Find an exchange
    def find_exchange_by_code(self, code):
        for exchange in self.EXCHANGES:
            if exchange.get_code() == code.lower():
                return exchange

    # Creates a structure of available assets (from_currency > to_currency > exchange)
    def _load_assets(self):
        self.assets = {}

        for exchange in self.EXCHANGES:
            if exchange.active:
                if not exchange.get_asset_pairs():
                    exchange.discover_assets(DownloadService(), lambda *args: None)
                self.assets[exchange.get_code()] = exchange.get_asset_pairs()

        # inverse the hierarchy for easier asset selection
        bases = {}
        for exchange in self.assets.keys():
            asset_pair = self.assets.get(exchange)
            for asset_pair in self.assets.get(exchange):
                base = asset_pair.get('base')
                quote = asset_pair.get('quote')

                if base not in bases:
                    bases[base] = {}

                if quote not in bases[base]:
                    bases[base][quote] = []

                bases[base][quote].append(self.find_exchange_by_code(exchange))

        self.bases = bases

    # load instances
    def _load_settings(self):
        self.settings = {}
        # load from file
        if isfile(SETTINGS_FILE):
            self.settings = yaml.load(open(SETTINGS_FILE, 'r'), Loader=yaml.SafeLoader)

        for plugin in self.settings.get('plugins', {}):
            for code, active in plugin.items():
                e = self.find_exchange_by_code(code)
                if e: e.active = active

        # set defaults if settings not defined
        if not self.settings.get('tickers'):

            # TODO work without defining a default 
            self.settings['tickers'] = [{
                'exchange': self.EXCHANGES[0].get_code(),
                'asset_pair': self.assets[self.EXCHANGES[0].get_code()][0].get('pair'),
                'refresh': 3,
                'default_label': self.EXCHANGES[0].get_default_label()
            }]

        if not self.settings.get('recent'):
            self.settings['recent'] = []

    # saves settings for each ticker
    def save_settings(self):
        tickers = []
        for instance in self.instances:
            ticker = {
                'exchange': instance.exchange.get_code(),
                'asset_pair': instance.exchange.asset_pair.get('pair'),
                'refresh': instance.refresh_frequency,
                'default_label': instance.default_label
            }
            tickers.append(ticker)
            self.settings['tickers'] = tickers

        plugins = []
        for exchange in self.EXCHANGES:
            plugin = {exchange.get_code(): exchange.active}
            plugins.append(plugin)

        self.settings['plugins'] = plugins

        try:
            with open(SETTINGS_FILE, 'w') as handle:
                yaml.dump(self.settings, handle, default_flow_style=False)
        except IOError:
            logging.error('Settings file not writable')

    # Add a new base to the recents settings, and push the last one off the edge
    def add_new_recent(self, asset_pair, exchange_code):
        for recent in self.settings['recent']:
            if recent.get('asset_pair') == asset_pair and recent.get('exchange') == exchange_code:
                self.settings['recent'].remove(recent)

        self.settings['recent'] = self.settings['recent'][0:4]

        new_recent = {
            'asset_pair': asset_pair,
            'exchange': exchange_code
        }

        self.settings['recent'].insert(0, new_recent)

        for instance in self.instances:
            instance.rebuild_recents_menu()

    # Start the main indicator icon and its menu
    def _start_main(self):
        print("{} v{} running!".format(
            self.config.get('app').get('name'),
            self.config.get('app').get('version')))

        self.icon = "{}/resources/icon_32px.png".format(
            self.config.get('project_root'))
        self.main_item = AppIndicator.Indicator.new(
            self.config.get('app').get('name'),
            self.icon,
            AppIndicator.IndicatorCategory.APPLICATION_STATUS)
        self.main_item.set_status(AppIndicator.IndicatorStatus.ACTIVE)
        self.main_item.set_ordering_index(0)
        self.main_item.set_menu(self._menu())

    def _start_gui(self):
        signal.signal(signal.SIGINT, Gtk.main_quit)  # ctrl+c exit
        DBusGMainLoop(set_as_default=True)
        bus = dbus.SystemBus()
        bus.add_signal_receiver(
            self.handle_resume,
            None,
            'org.freedesktop.login1.Manager',
            'org.freedesktop.login1'
        )
        Gtk.main()

    # Program main menu
    def _menu(self):
        menu = Gtk.Menu()

        self.add_item = Gtk.MenuItem.new_with_label("Add Ticker")
        self.discover_item = Gtk.MenuItem.new_with_label("Discover Assets")
        self.plugin_item = Gtk.MenuItem.new_with_label("Plugins" + u"\u2026")
        self.about_item = Gtk.MenuItem.new_with_label("About")
        self.quit_item = Gtk.MenuItem.new_with_label("Quit")

        self.add_item.connect("activate", self._add_ticker)
        self.discover_item.connect("activate", self._discover_assets)
        self.plugin_item.connect("activate", self._select_plugins)
        self.about_item.connect("activate", self._about)
        self.quit_item.connect("activate", self._quit_all)

        menu.append(self.add_item)
        menu.append(self.discover_item)
        menu.append(self.plugin_item)
        menu.append(self.about_item)
        menu.append(Gtk.SeparatorMenuItem())
        menu.append(self.quit_item)
        menu.show_all()

        return menu

    # Adds a ticker and starts it
    def _add_indicator(self, settings):
        exchange = settings.get('exchange')
        refresh = settings.get('refresh')
        asset_pair = settings.get('asset_pair')
        default_label = settings.get('default_label')
        self.unique_id += 1
        indicator = Indicator(
            self,
            self.unique_id,
            exchange,
            asset_pair,
            refresh,
            default_label)
        self.instances.append(indicator)
        indicator.start()
        return indicator

    # adds many tickers
    def _add_many_indicators(self, tickers):
        for ticker in tickers:
            self._add_indicator(ticker)

    # Menu item to add a ticker
    def _add_ticker(self, widget):
        i = self._add_indicator(self.settings.get('tickers')[len(self.settings.get('tickers')) - 1])
        i._settings(widget)
        self.save_settings()

    # Remove ticker
    def remove_ticker(self, indicator):
        if len(self.instances) == 1:  # is it the last ticker?
            Gtk.main_quit()  # then quit entirely
        else:  # otherwise just remove this one
            indicator.exchange.stop()
            indicator.indicator_widget.set_status(AppIndicator.IndicatorStatus.PASSIVE)
            self.instances.remove(indicator)
            self.save_settings()

    # Menu item to download any new assets from the exchanges
    def _discover_assets(self, widget):
        # Don't do anything if there are no active exchanges with discovery
        if len([ex for ex in self.EXCHANGES if ex.active and ex.discovery]) == 0:
            return

        self.main_item.set_icon_full(self.config.get('project_root') + '/resources/loading.png', 'Discovering assets')

        for indicator in self.instances:
            if indicator.asset_selection_window:
                indicator.asset_selection_window.destroy()

        for exchange in self.EXCHANGES:
            if exchange.active and exchange.discovery:
                exchange.discover_assets(self.downloader, self.update_assets)

    # When discovery completes, reload currencies and rebuild menus of all instances
    def update_assets(self):
        self.discoveries += 1
        if self.discoveries < len([ex for ex in self.EXCHANGES if ex.active and ex.discovery]):
            return  # wait until all active exchanges with discovery finish discovery

        self.discoveries = 0
        self._load_assets()

        if notify2.init(self.config.get('app').get('name')):
            n = notify2.Notification(
                self.config.get('app').get('name'),
                "Finished discovering new assets",
                self.icon)
            n.set_urgency(1)
            n.timeout = 2000
            n.show()

        self.main_item.set_icon_full(self.icon, "App icon")

    # Handle system resume by refreshing all tickers
    def handle_resume(self, sleeping, *args):
        if not sleeping:
            for instance in self.instances:
                instance.exchange.stop().start()

    def _select_plugins(self, widget):
        PluginSelectionWindow(self)

    # Menu item to remove all tickers and quits the application
    def _quit_all(self, widget):
        Gtk.main_quit()

    def plugins_updated(self):
        self._load_assets()
        for instance in self.instances:
            instance.start()  # will stop exchange if inactive

        self.save_settings()

    def _about(self, widget):
        AboutWindow(self.config).show()


coin = Coin()