#! /usr/bin/python
# -*- coding: utf-8 -*-

# Copyright(C) 2013 Mark Tully <markjtully@gmail.com>
# Copyright(C) 2012 Bernd Schlapsi <brot@gmx.info>
# This program is free software: you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 3, as published
# by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranties of
# MERCHANTABILITY, SATISFACTORY QUALITY, 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/>.

from gi.repository import GLib, Gio
from gi.repository import Unity, UnityExtras
import gettext
import os.path
import paramiko

APP_NAME = 'unity-scope-sshsearch'
LOCAL_PATH = '/usr/share/locale/'
gettext.bindtextdomain(APP_NAME, LOCAL_PATH)
gettext.textdomain(APP_NAME)
_ = gettext.gettext

BUS_NAME = 'com.canonical.Unity.Scope.Boxes.Sshsearch'
BUS_PATH = '/com/canonical/unity/scope/boxes/sshsearch'

SVG_DIR = '/usr/share/icons/unity-icon-theme/places/svg/'
CAT_0_ICON = Gio.ThemedIcon.new(SVG_DIR + 'group-boxes.svg')
CAT_0_TITLE = _('Boxes')

NO_RESULTS_HINT = _('Sorry, there are no available SSH connections that match your search.')
SEARCH_HINT = _('Search for available SSH connections')

SSH_DEFAULT_PORT = '22'
SSHCONFIG = os.path.join('~', '.ssh', 'config')
SSHCONFIG_EXPAND = os.path.expanduser(SSHCONFIG)
KNOWN_HOSTS = os.path.join('~', '.ssh', 'known_hosts')
KNOWN_HOSTS_EXPAND = os.path.expanduser(KNOWN_HOSTS)

TERMINAL_APP = 'gnome-terminal'
TERMINAL_APP_MIMETYPE = 'application/x-desktop'


class Daemon:
    def __init__(self):
        self.scope = Unity.DeprecatedScope.new(BUS_PATH, 'sshsearch')
        self.scope.props.search_hint = SEARCH_HINT
        self.scope.search_in_global = True
        cats = Unity.CategorySet.new()
        cats.add(Unity.Category.new(CAT_0_TITLE.lower(),
                                    CAT_0_TITLE,
                                    CAT_0_ICON,
                                    Unity.CategoryRenderer.VERTICAL_TILE))
        self.scope.props.categories = cats
        self.preferences = Unity.PreferencesManager.get_default()
        self.preferences.connect('notify::remote-content-search', self._on_preference_changed)
        self.scope.connect('search-changed', self.on_search_changed)
        self.scope.connect("activate-uri", self.on_activate_uri)
        self.scope.connect("preview-uri", self.on_preview_uri)
        self.scope.export()

    def __read_config(self, filemonitor, file, other_file, event_type):
        if not file.query_exists(None):
            self._config_hosts = []
            return
        if (event_type in (Gio.FileMonitorEvent.CREATED,
                           Gio.FileMonitorEvent.CHANGED,
                           Gio.FileMonitorEvent.CHANGES_DONE_HINT,
                           Gio.FileMonitorEvent.ATTRIBUTE_CHANGED)):
            c = paramiko.SSHConfig()
            c.parse(open(file.get_path()))
            self._config_hosts = [h['host'].lower() for h in c._config if '*' not in h['host']]

    def __read_known_hosts(self, filemonitor, file, other_file, event_type):
        if not file.query_exists(None):
            self._known_hosts = []
            return
        if (event_type in (Gio.FileMonitorEvent.CREATED,
                           Gio.FileMonitorEvent.CHANGED,
                           Gio.FileMonitorEvent.CHANGES_DONE_HINT,
                           Gio.FileMonitorEvent.ATTRIBUTE_CHANGED)):
            h = paramiko.HostKeys(KNOWN_HOSTS_EXPAND)
            self._known_hosts = [host for host in h.keys() if len(host) != 60]

    def __parse_hoststring(self, hoststring, user):
        # assign host and port
        host = hoststring
        port = SSH_DEFAULT_PORT
        if hoststring.startswith('['):
            host, port = hoststring[1:].split(']:')

        # assign target
        target = host
        if user:
            target = '%s@%s' % (user, host)

        # assign connection-description
        conn_desc = target
        if port != SSH_DEFAULT_PORT:
            conn_desc = '%s:%s' % (target, port)

        return target, port, conn_desc

    def _on_preference_changed(self, *_):
        self.scope.queue_search_changed(Unity.SearchType.DEFAULT)

    def on_search_changed(self, scope, search, search_type, *_):
        model = search.props.results_model
        model.clear()
        if self.preferences.props.remote_content_search != Unity.PreferencesManagerRemoteContent.ALL:
            search.emit('finished')
            return
        search_string = search.props.search_string.strip()
        print('Search changed to \'%s\'' % search_string)
        self.update_results_model(search_string, model)
        search.set_reply_hint('no-results-hint',
                              GLib.Variant.new_string(NO_RESULTS_HINT))
        search.emit('finished')

    def update_results_model(self, search, results):

        # read/parse ssh-config file
        self._config_hosts = []
        self._config_file = Gio.file_new_for_path(SSHCONFIG_EXPAND)
        self._config_monitor = self._config_file.monitor_file(
            flags=Gio.FileMonitorFlags.NONE, cancellable=None)
        self._config_monitor.connect('changed', self.__read_config)
        self.__read_config(None, self._config_file, None, Gio.FileMonitorEvent.CREATED)

        # read/parse ssh-known_hosts file
        self._known_hosts = []
        self._knownhosts_file = Gio.file_new_for_path(KNOWN_HOSTS_EXPAND)
        self._knownhosts_monitor = self._knownhosts_file.monitor_file(flags=Gio.FileMonitorFlags.NONE, cancellable=None)
        self.__read_known_hosts(None, self._knownhosts_file, None, Gio.FileMonitorEvent.CREATED)

        searchparts = search.split('@')
        searchhost = searchparts[-1]
        searchuser = ''
        if len(searchparts) == 2:
            searchuser = searchparts[0]

        found = [host for host in self._config_hosts if host.find(searchhost) >= 0]
        for host in found:
            target, port, conn_desc = self.__parse_hoststring(host, searchuser)
            results.append(uri='ssh://config/%s/%s' % (searchuser, host),
                           icon_hint=TERMINAL_APP,
                           category=0,
                           mimetype=TERMINAL_APP_MIMETYPE,
                           title=conn_desc,
                           comment=conn_desc,
                           dnd_uri='',
                           result_type=Unity.ResultType.DEFAULT)

        found = [host for host in self._known_hosts if host.find(searchhost) >= 0]
        for host in found:
            target, port, conn_desc = self.__parse_hoststring(host, searchuser)
            results.append(uri='ssh://config/%s/%s' % (searchuser, host),
                           icon_hint=TERMINAL_APP,
                           category=1,
                           mimetype=TERMINAL_APP_MIMETYPE,
                           title=conn_desc,
                           comment=conn_desc,
                           dnd_uri='',
                           result_type=Unity.ResultType.DEFAULT)

    def on_activate_uri(self, scope, uri):
        uri_splitted = uri.split('/')
        hoststring = uri_splitted[-1]
        user = uri_splitted[-2]
        target, port, conn_desc = self.__parse_hoststring(hoststring, user)

        if port == SSH_DEFAULT_PORT:
            # don't call with the port option, because the host definition
            # could be from the ~/.ssh/config file
            GLib.spawn_command_line_async('%s -e "ssh %s"' % (TERMINAL_APP, target))
        else:
            GLib.spawn_command_line_async('%s -e "ssh -p %s %s"' % (TERMINAL_APP, port, target))

        return Unity.ActivationResponse(handled=Unity.HandledType.HIDE_DASH, goto_uri='')

    def on_preview_uri(self, scope, uri):
        uri_splitted = uri.split('/')
        hoststring = uri_splitted[-1]
        user = uri_splitted[-2]
        target, port, conn_desc = self.__parse_hoststring(hoststring, user)

        image = Gio.ThemedIcon.new(TERMINAL_APP)
        preview = Unity.GenericPreview.new(conn_desc, '', image)
        action = Unity.PreviewAction.new("open", _("Connect"), None)
        action.connect("activated", self.on_activate_uri)
        preview.add_action(action)
        return preview


if __name__ == '__main__':
    daemon = UnityExtras.dbus_own_name(BUS_NAME, Daemon, None)
    if daemon:
        GLib.unix_signal_add(0, 2, lambda x: daemon.quit(), None)
        daemon.run([])
