# Terminator by Chris Jones <cmsj@tenshu.net>
# GPL v2 only
"""ipc.py - DBus server and API calls"""

import sys
import hashlib
from gi.repository import Gdk
import dbus.service
from dbus.exceptions import DBusException
import dbus.glib
from .borg import Borg
from .terminator import Terminator
from .config import Config
from .factory import Factory
from .util import dbg, err, enumerate_descendants
from .terminal import Terminal
from .container import Container
from .configjson import ConfigJson
from gi.repository import Gtk as gtk
from gi.repository import GObject as gobject

CONFIG = Config()
if not CONFIG['dbus']:
    # The config says we are not to load dbus, so pretend like we can't
    dbg('dbus disabled')
    raise ImportError

BUS_BASE = 'net.tenshu.Terminator2'
BUS_PATH = '/net/tenshu/Terminator2'
try:
    # Try and include the X11 display name in the dbus bus name
    DISPLAY = Gdk.get_display().partition('.')[0]
    # In Python 3, hash() uses a different seed on each run, so use hashlib
    DISPLAY = hashlib.md5(DISPLAY.encode('utf-8')).hexdigest()
    BUS_NAME = '%s%s' % (BUS_BASE, DISPLAY)
except:
    BUS_NAME = BUS_BASE

class DBusService(Borg, dbus.service.Object):
    """DBus Server class. This is implemented as a Borg"""
    bus_name = None
    bus_path = None
    terminator = None

    def __init__(self):
        """Class initialiser"""
        Borg.__init__(self, self.__class__.__name__)
        self.prepare_attributes()
        dbus.service.Object.__init__(self, self.bus_name, BUS_PATH)

    def prepare_attributes(self):
        """Ensure we are populated"""
        if not self.bus_name:
            dbg('Checking for bus name availability: %s' % BUS_NAME)
            try:
                bus = dbus.SessionBus()
            except Exception as e:
                err('Unable to connect to DBUS Server, proceeding as standalone')
                raise ImportError
            proxy = bus.get_object('org.freedesktop.DBus',
                                   '/org/freedesktop/DBus')
            flags = 1 | 4 # allow replacement | do not queue
            if not proxy.RequestName(BUS_NAME, dbus.UInt32(flags)) in (1, 4):
                dbg('bus name unavailable: %s' % BUS_NAME)
                raise dbus.exceptions.DBusException(
                    "Couldn't get DBus name %s: Name exists" % BUS_NAME)
            self.bus_name = dbus.service.BusName(BUS_NAME,
                                                 bus=dbus.SessionBus())
        if not self.bus_path:
            self.bus_path = BUS_PATH
        if not self.terminator:
            self.terminator = Terminator()

    @dbus.service.method(BUS_NAME, in_signature='a{ss}')
    def new_window_cmdline(self, options=dbus.Dictionary()):
        """Create a new Window"""
        dbg('dbus method called: new_window with parameters %s'%(options))
        if options['configjson']:
            dbg(options['configjson'])
            configjson = ConfigJson()
            layoutname = configjson.extend_config(options['configjson'])
            if layoutname and ((not options['layout']) or options['layout'] == 'default'):
                options['layout'] = layoutname
                if not options['profile']:
                    options['profile'] = configjson.get_profile_to_use()

        oldopts = self.terminator.config.options_get()
        oldopts.__dict__ = options
        self.terminator.config.options_set(oldopts)
        self.terminator.create_layout(oldopts.layout)
        self.terminator.layout_done()

    @dbus.service.method(BUS_NAME, in_signature='a{ss}')
    def new_tab_cmdline(self, options=dbus.Dictionary()):
        """Create a new tab"""
        dbg('dbus method called: new_tab with parameters %s'%(options))
        oldopts = self.terminator.config.options_get()
        oldopts.__dict__ = options
        self.terminator.config.options_set(oldopts)
        window = self.terminator.get_windows()[0]
        window.tab_new()

    @dbus.service.method(BUS_NAME, in_signature='a{ss}')
    def unhide_cmdline(self,options=dbus.Dictionary):
        dbg('unhide_cmdline')
        for window in self.terminator.get_windows():
            if not window.get_property('visible'):
                window.on_hide_window()

    @dbus.service.method(BUS_NAME)
    def new_window(self):
        """Create a new Window"""
        terminals_before = set(self.get_terminals())
        self.terminator.new_window()
        terminals_after = set(self.get_terminals())
        new_terminal_set = list(terminals_after - terminals_before)
        if len(new_terminal_set) != 1:
            return "ERROR: Cannot determine the UUID of the added terminal"
        else:
            return new_terminal_set[0]

    @dbus.service.method(BUS_NAME)
    def new_tab(self, uuid=None):
        """Create a new tab"""
        return self.new_terminal(uuid, 'tab')

    @dbus.service.method(BUS_NAME)
    def reload_configuration(self):
        """Reload configuration for all terminals"""
        self.terminator.config.base.reload()
        self.terminator.reconfigure()

    @dbus.service.method(BUS_NAME) 
    def bg_img_all (self,options=dbus.Dictionary()):
        for terminal in self.terminator.terminals:
            terminal.set_background_image(options.get('file')) 
            
    @dbus.service.method(BUS_NAME) 
    def bg_img(self,uuid=None,options=dbus.Dictionary()):
        self.terminator.find_terminal_by_uuid(uuid).set_background_image(options.get('file'))

    @dbus.service.method(BUS_NAME)
    def hsplit(self, uuid=None,options=None):
        """Split a terminal horizontally, by UUID"""
        if options:
            cmd = options.get('execute')
            title = options.get('title')
            return self.new_terminal_cmd(uuid=uuid, title=title, cmd=cmd, split_vert=True) 
        else:
            return self.new_terminal(uuid, 'hsplit')

    @dbus.service.method(BUS_NAME)
    def vsplit(self, uuid=None,options=None):
        """Split a terminal vertically, by UUID"""
        if options:
            cmd = options.get('execute')
            title = options.get('title')
            return self.new_terminal_cmd(uuid=uuid, title=title, cmd=cmd, split_vert=False) 
        else:
            return self.new_terminal(uuid, 'vsplit')

    def get_terminal_container(self, terminal, container=None):
        terminator = Terminator()
        if not container:
            for window in terminator.windows:
                owner = self.get_terminal_container(terminal, window)
                if owner: return owner
        else:
            for child in container.get_children():
                if isinstance(child, Terminal) and child == terminal:
                    return container
                if isinstance(child, Container):
                    owner = self.get_terminal_container(terminal, child)
                    if owner: return owner

    def new_terminal_cmd(self, uuid=None, title=None, cmd=None, split_vert=False):
        """Split a terminal by UUID and immediately runs the specified command in the new terminal"""
        if not uuid:
            return "ERROR: No UUID specified"

        terminal = self.terminator.find_terminal_by_uuid(uuid)

        terminals_before = set(self.get_terminals())
        if not terminal:
            return "ERROR: Terminal with supplied UUID not found"

        # get current working dir out of target terminal
        cwd = terminal.get_cwd()

        # get current container
        container = self.get_terminal_container(terminal)
        maker = Factory()
        sibling = maker.make('Terminal')
        sibling.set_cwd(cwd)
        if title: sibling.titlebar.set_custom_string(title)
        sibling.spawn_child(init_command=cmd)

        # split and run command in new terminal
        container.split_axis(terminal, split_vert, cwd, sibling)

        terminals_after = set(self.get_terminals())
        # Detect the new terminal UUID
        new_terminal_set = list(terminals_after - terminals_before)
        if len(new_terminal_set) != 1:
            return "ERROR: Cannot determine the UUID of the added terminal"
        else:
            return new_terminal_set[0]

    def new_terminal(self, uuid, type):
        """Split a terminal horizontally o?r vertically, by UUID"""
        dbg('dbus method called: %s' % type)
        if not uuid:
            return "ERROR: No UUID specified"
        terminal = self.terminator.find_terminal_by_uuid(uuid)
        terminals_before = set(self.get_terminals())
        if not terminal:
            return "ERROR: Terminal with supplied UUID not found"
        elif type == 'tab':
            terminal.key_new_tab()
        elif type == 'hsplit':
            terminal.key_split_horiz()
        elif type == 'vsplit':
            terminal.key_split_vert()
        else:
            return "ERROR: Unknown type \"%s\" specified" % (type)
        terminals_after = set(self.get_terminals())
        # Detect the new terminal UUID
        new_terminal_set = list(terminals_after - terminals_before)
        if len(new_terminal_set) != 1:
            return "ERROR: Cannot determine the UUID of the added terminal"
        else:
            return new_terminal_set[0]

    @dbus.service.method(BUS_NAME)
    def get_terminals(self):
        """Return a list of all the terminals"""
        return [x.uuid.urn for x in self.terminator.terminals]

    @dbus.service.method(BUS_NAME)
    def get_focused_terminal(self):
        """Returns the uuid of the currently focused terminal"""
        if self.terminator.last_focused_term:
            return self.terminator.last_focused_term.uuid.urn
        return None

    @dbus.service.method(BUS_NAME)
    def get_window(self, uuid=None):
        """Return the UUID of the parent window of a given terminal"""
        terminal = self.terminator.find_terminal_by_uuid(uuid)
        window = terminal.get_toplevel()
        return window.uuid.urn

    @dbus.service.method(BUS_NAME)
    def get_window_title(self, uuid=None):
        """Return the title of a parent window of a given terminal"""
        terminal = self.terminator.find_terminal_by_uuid(uuid)
        window = terminal.get_toplevel()
        return window.get_title()

    @dbus.service.method(BUS_NAME)
    def get_tab(self, uuid=None):
        """Return the UUID of the parent tab of a given terminal"""
        maker = Factory()
        terminal = self.terminator.find_terminal_by_uuid(uuid)
        window = terminal.get_toplevel()
        root_widget = window.get_children()[0]
        if maker.isinstance(root_widget, 'Notebook'):
            #return root_widget.uuid.urn
            for tab_child in root_widget.get_children():
                terms = [tab_child]
                if not maker.isinstance(terms[0], "Terminal"):
                    terms = enumerate_descendants(tab_child)[1]
                if terminal in terms:
                    # FIXME: There are no uuid's assigned to the the notebook, or the actual tabs!
                    # This would fail: return root_widget.uuid.urn
                    return ""

    @dbus.service.method(BUS_NAME)
    def get_tab_title(self, uuid=None):
        """Return the title of a parent tab of a given terminal"""
        maker = Factory()
        terminal = self.terminator.find_terminal_by_uuid(uuid)
        window = terminal.get_toplevel()
        root_widget = window.get_children()[0]
        if maker.isinstance(root_widget, "Notebook"):
            for tab_child in root_widget.get_children():
                terms = [tab_child]
                if not maker.isinstance(terms[0], "Terminal"):
                    terms = enumerate_descendants(tab_child)[1]
                if terminal in terms:
                    return root_widget.get_tab_label(tab_child).get_label()

    @dbus.service.method(BUS_NAME)
    def set_tab_title(self, uuid=None, options=dbus.Dictionary()):
        """Set the title of a parent tab of a given terminal"""
        tab_title = options.get('tab-title')

        maker = Factory()
        terminal = self.terminator.find_terminal_by_uuid(uuid)
        window = terminal.get_toplevel()

        if not window.is_child_notebook():
            return

        notebook = window.get_children()[0]
        n_page = notebook.get_current_page()
        page = notebook.get_nth_page(n_page)
        label = notebook.get_tab_label(page)
        label.set_custom_label(tab_title, force=True)

    @dbus.service.method(BUS_NAME)
    def switch_profile(self, uuid=None, options=dbus.Dictionary()):
        """Switch profile of a given terminal"""
        terminal = self.terminator.find_terminal_by_uuid(uuid)
        profile_name = options.get('profile')
        terminal.force_set_profile(False, profile_name)

    @dbus.service.method(BUS_NAME)
    def switch_profile_all(self, options=dbus.Dictionary()):
        """Switch profile of a given terminal"""
        for terminal in self.terminator.terminals:
            profile_name = options.get('profile')
            terminal.force_set_profile(False, profile_name)


def with_proxy(func):
    """Decorator function to connect to the session dbus bus"""
    dbg('dbus client call: %s' % func.__name__)
    def _exec(*args, **argd):
        bus = dbus.SessionBus()
        try:
            proxy = bus.get_object(BUS_NAME, BUS_PATH)

        except dbus.DBusException as e:
            sys.exit(
                "Remotinator can't connect to terminator. " +
                "May be terminator is not running.")

        return func(proxy, *args, **argd)
    return _exec

@with_proxy
def new_window_cmdline(session, options):
    """Call the dbus method to open a new window"""
    session.new_window_cmdline(options)

@with_proxy
def new_tab_cmdline(session, options):
    """Call the dbus method to open a new tab in the first window"""
    session.new_tab_cmdline(options)

@with_proxy
def reload_configuration(session):
    """Call the dbus method to reload configuration for all windows"""
    session.reload_configuration()

@with_proxy
def unhide_cmdline(session,options):
    session.unhide_cmdline(options)

@with_proxy
def new_window(session, options):
    """Call the dbus method to open a new window"""
    print(session.new_window())

@with_proxy
def new_tab(session, uuid, options):
    """Call the dbus method to open a new tab in the first window"""
    print(session.new_tab(uuid))

@with_proxy
def hsplit(session, uuid, options):
    """Call the dbus method to horizontally split a terminal"""
    print(session.hsplit(uuid,options))

@with_proxy
def vsplit(session, uuid, options):
    """Call the dbus method to vertically split a terminal"""
    print(session.vsplit(uuid,options))

@with_proxy
def get_terminals(session, options):
    """Call the dbus method to return a list of all terminals"""
    print('\n'.join(session.get_terminals()))

@with_proxy
def get_focused_terminal(session, options):
    """Call the dbus method to return the currently focused terminal"""
    return session.get_focused_terminal()

@with_proxy
def get_window(session, uuid, options):
    """Call the dbus method to return the toplevel tab for a terminal"""
    print(session.get_window(uuid))

@with_proxy
def get_window_title(session, uuid, options):
    """Call the dbus method to return the title of a tab"""
    print(session.get_window_title(uuid))

@with_proxy
def get_tab(session, uuid, options):
    """Call the dbus method to return the toplevel tab for a terminal"""
    print(session.get_tab(uuid))

@with_proxy
def get_tab_title(session, uuid, options):
    """Call the dbus method to return the title of a tab"""
    print(session.get_tab_title(uuid))

@with_proxy
def set_tab_title(session, uuid, options):
    """Call the dbus method to set the title of a tab"""
    session.set_tab_title(uuid, options)

@with_proxy
def switch_profile(session, uuid, options):
    """Call the dbus method to return the title of a tab"""
    session.switch_profile(uuid, options)

@with_proxy
def switch_profile_all(session,options):
    """Call the dbus method to return the title of a tab"""
    session.switch_profile_all(options)

@with_proxy
def bg_img_all(session,options):
    session.bg_img_all(options)

@with_proxy
def bg_img(session,uuid,options):
    session.bg_img(uuid,options)
