#!/usr/bin/python2
# Terminator by Chris Jones <cmsj@tenshu.net>
# GPL v2 only
"""terminator.py - class for the master Terminator singleton"""

import copy
import os
import gtk

from borg import Borg
from config import Config
from keybindings import Keybindings
from util import dbg, err, enumerate_descendants
from factory import Factory
from cwd import get_pid_cwd
from version import APP_NAME, APP_VERSION

class Terminator(Borg):
    """master object for the application"""

    windows = None
    launcher_windows = None
    windowtitle = None
    terminals = None
    groups = None
    config = None
    keybindings = None

    origcwd = None
    dbus_path = None
    dbus_name = None
    pid_cwd = None
    gnome_client = None
    debug_address = None

    doing_layout = None
    layoutname = None
    last_active_window = None

    groupsend = None
    groupsend_type = {'all':0, 'group':1, 'off':2}

    def __init__(self):
        """Class initialiser"""

        Borg.__init__(self, self.__class__.__name__)
        self.prepare_attributes()

    def prepare_attributes(self):
        """Initialise anything that isn't already"""

        if not self.windows:
            self.windows = []
        if not self.launcher_windows:
            self.launcher_windows = []
        if not self.terminals:
            self.terminals = []
        if not self.groups:
            self.groups = []
        if not self.config:
            self.config = Config()
        if self.groupsend == None:
            self.groupsend = self.groupsend_type[self.config['broadcast_default']]
        if not self.keybindings:
            self.keybindings = Keybindings()
            self.keybindings.configure(self.config['keybindings'])
        if not self.doing_layout:
            self.doing_layout = False
        if not self.pid_cwd:
            self.pid_cwd = get_pid_cwd()
        if self.gnome_client is None:
            self.attempt_gnome_client()

    def set_origcwd(self, cwd):
        """Store the original cwd our process inherits"""
        if cwd == '/':
            cwd = os.path.expanduser('~')
            os.chdir(cwd)
        self.origcwd = cwd

    def set_dbus_data(self, dbus_service):
        """Store the DBus bus details, if they are available"""
        if dbus_service:
            self.dbus_name = dbus_service.bus_name.get_name()
            self.dbus_path = dbus_service.bus_path

    def attempt_gnome_client(self):
        """Attempt to find a GNOME Session to register with"""
        try:
            import gnome
            import gnome.ui
            self.gnome_program = gnome.init(APP_NAME, APP_VERSION)
            self.gnome_client = gnome.ui.master_client()
            self.gnome_client.connect_to_session_manager()
            self.gnome_client.connect('save-yourself', self.save_yourself)
            self.gnome_client.connect('die', self.die)
            dbg('GNOME session support enabled and registered')
        except (ImportError, AttributeError):
            self.gnome_client = False
            dbg('GNOME session support not available')

    def save_yourself(self, *args):
        """Save as much state as possible for the session manager"""
        dbg('preparing session manager state')
        # FIXME: Implement this

    def die(self, *args):
        """Die at the hands of the session manager"""
        dbg('session manager asked us to die')
        # FIXME: Implement this

    def get_windows(self):
        """Return a list of windows"""
        return self.windows

    def register_window(self, window):
        """Register a new window widget"""
        if window not in self.windows:
            dbg('Terminator::register_window: registering %s:%s' % (id(window),
                type(window)))
            self.windows.append(window)

    def deregister_window(self, window):
        """de-register a window widget"""
        dbg('Terminator::deregister_window: de-registering %s:%s' %
                (id(window), type(window)))
        if window in self.windows:
            self.windows.remove(window)
        else:
            err('%s is not in registered window list' % window)

        if len(self.windows) == 0:
            # We have no windows left, we should exit
            dbg('no windows remain, quitting')
            gtk.main_quit()

    def register_launcher_window(self, window):
        """Register a new launcher window widget"""
        if window not in self.launcher_windows:
            dbg('Terminator::register_launcher_window: registering %s:%s' % (id(window),
                type(window)))
            self.launcher_windows.append(window)

    def deregister_launcher_window(self, window):
        """de-register a launcher window widget"""
        dbg('Terminator::deregister_launcher_window: de-registering %s:%s' %
                (id(window), type(window)))
        if window in self.launcher_windows:
            self.launcher_windows.remove(window)
        else:
            err('%s is not in registered window list' % window)

        if len(self.launcher_windows) == 0 and len(self.windows) == 0:
            # We have no windows left, we should exit
            dbg('no windows remain, quitting')
            gtk.main_quit()

    def register_terminal(self, terminal):
        """Register a new terminal widget"""
        if terminal not in self.terminals:
            dbg('Terminator::register_terminal: registering %s:%s' %
                    (id(terminal), type(terminal)))
            self.terminals.append(terminal)

    def deregister_terminal(self, terminal):
        """De-register a terminal widget"""
        dbg('Terminator::deregister_terminal: de-registering %s:%s' %
                (id(terminal), type(terminal)))
        self.terminals.remove(terminal)

        if len(self.terminals) == 0:
            dbg('no terminals remain, destroying all windows')
            for window in self.windows:
                window.destroy()
        else:
            dbg('Terminator::deregister_terminal: %d terminals remain' %
                    len(self.terminals))

    def find_terminal_by_uuid(self, uuid):
        """Search our terminals for one matching the supplied UUID"""
        dbg('searching self.terminals for: %s' % uuid)
        for terminal in self.terminals:
            dbg('checking: %s (%s)' % (terminal.uuid.urn, terminal))
            if terminal.uuid.urn == uuid:
                return terminal
        return None

    def new_window(self, cwd=None):
        """Create a window with a Terminal in it"""
        maker = Factory()
        window = maker.make('Window')
        terminal = maker.make('Terminal')
        if cwd:
            terminal.set_cwd(cwd)
        window.add(terminal)
        window.show(True)
        terminal.spawn_child()

        return(window, terminal)

    def create_layout(self, layoutname):
        """Create all the parts necessary to satisfy the specified layout"""
        layout = None
        objects = {}

        self.doing_layout = True

        layout = copy.deepcopy(self.config.layout_get_config(layoutname))
        if not layout:
            # User specified a non-existent layout. default to one Terminal
            err('layout %s not defined' % layout)
            self.new_window()
            return

        # Wind the flat objects into a hierarchy
        hierarchy = {}
        count = 0
        # Loop over the layout until we have consumed it, or hit 1000 loops.
        # This is a stupid artificial limit, but it's safe.
        while len(layout) > 0 and count < 1000:
            count = count + 1
            if count == 1000:
                err('hit maximum loop boundary. THIS IS VERY LIKELY A BUG')
            for obj in layout.keys():
                if layout[obj]['type'].lower() == 'window':
                    hierarchy[obj] = {}
                    hierarchy[obj]['type'] = 'Window'
                    hierarchy[obj]['children'] = {}

                    # Copy any additional keys
                    for objkey in layout[obj].keys():
                        if layout[obj][objkey] != '' and not hierarchy[obj].has_key(objkey):
                            hierarchy[obj][objkey] = layout[obj][objkey]

                    objects[obj] = hierarchy[obj]
                    del(layout[obj])
                else:
                    # Now examine children to see if their parents exist yet
                    if not layout[obj].has_key('parent'):
                        err('Invalid object: %s' % obj)
                        del(layout[obj])
                        continue
                    if objects.has_key(layout[obj]['parent']):
                        # Our parent has been created, add ourselves
                        childobj = {}
                        childobj['type'] = layout[obj]['type']
                        childobj['children'] = {}

                        # Copy over any additional object keys
                        for objkey in layout[obj].keys():
                            if not childobj.has_key(objkey):
                                childobj[objkey] = layout[obj][objkey]

                        objects[layout[obj]['parent']]['children'][obj] = childobj
                        objects[obj] = childobj
                        del(layout[obj])

        layout = hierarchy

        for windef in layout:
            if layout[windef]['type'] != 'Window':
                err('invalid layout format. %s' % layout)
                raise(ValueError)
            dbg('Creating a window')
            window, terminal = self.new_window()
            if layout[windef].has_key('position'):
                parts = layout[windef]['position'].split(':')
                if len(parts) == 2:
                    window.move(int(parts[0]), int(parts[1]))
            if layout[windef].has_key('size'):
                parts = layout[windef]['size']
                winx = int(parts[0])
                winy = int(parts[1])
                if winx > 1 and winy > 1:
                    window.resize(winx, winy)
            if layout[windef].has_key('title'):
                window.title.force_title(layout[windef]['title'])
            if layout[windef].has_key('maximised'):
                if layout[windef]['maximised'] == 'True':
                    window.ismaximised = True
                else:
                    window.ismaximised = False
                window.set_maximised(window.ismaximised)
            if layout[windef].has_key('fullscreen'):
                if layout[windef]['fullscreen'] == 'True':
                    window.isfullscreen = True
                else:
                    window.isfullscreen = False
                window.set_fullscreen(window.isfullscreen)
            window.create_layout(layout[windef])

        self.layoutname = layoutname

    def layout_done(self):
        """Layout operations have finished, record that fact"""
        self.doing_layout = False
        maker = Factory()

        window_last_active_term_mapping = {}
        for window in self.windows:
            if window.is_child_notebook():
                source = window.get_toplevel().get_children()[0]
            else:
                source = window
            window_last_active_term_mapping[window] = copy.copy(source.last_active_term)

        for terminal in self.terminals:
            if not terminal.pid:
                terminal.spawn_child()

        for window in self.windows:
            if window.is_child_notebook():
                # For windows with a notebook
                notebook = window.get_toplevel().get_children()[0]
                # Cycle through pages by number
                for page in xrange(0, notebook.get_n_pages()):
                    # Try and get the entry in the previously saved mapping
                    mapping = window_last_active_term_mapping[window]
                    page_last_active_term = mapping.get(notebook.get_nth_page(page),  None)
                    if page_last_active_term is None:
                        # Couldn't find entry, so we find the first child of type Terminal
                        children = notebook.get_nth_page(page).get_children()
                        for page_last_active_term in children:
                            if maker.isinstance(page_last_active_term, 'Terminal'):
                                page_last_active_term = page_last_active_term.uuid
                                break
                        else:
                            err('Should never reach here!')
                            page_last_active_term = None
                    if page_last_active_term is None:
                        # Bail on this tab as we're having no luck here, continue with the next
                        continue
                    # Set the notebook entry, then ensure Terminal is visible and focussed
                    urn = page_last_active_term.urn
                    notebook.last_active_term[notebook.get_nth_page(page)] = page_last_active_term
                    if urn:
                        term = self.find_terminal_by_uuid(urn)
                        if term:
                            term.ensure_visible_and_focussed()
            else:
                # For windows without a notebook ensure Terminal is visible and focussed
                if window_last_active_term_mapping[window]:
                    term = self.find_terminal_by_uuid(window_last_active_term_mapping[window].urn)
                    term.ensure_visible_and_focussed()

        for window in self.windows:
            if window.uuid == self.last_active_window:
                window.show()

    def reconfigure(self):
        """Update configuration for the whole application"""

        if self.config['handle_size'] in xrange(0, 6):
            gtk.rc_parse_string("""
                style "terminator-paned-style" {
                    GtkPaned::handle_size = %s 
                }
                class "GtkPaned" style "terminator-paned-style" 
                """ % self.config['handle_size'])
            gtk.rc_reset_styles(gtk.settings_get_default())

        # Cause all the terminals to reconfigure
        for terminal in self.terminals:
            terminal.reconfigure()

        # Reparse our keybindings
        self.keybindings.configure(self.config['keybindings'])

        # Update tab position if appropriate
        maker = Factory()
        for window in self.windows:
            child = window.get_child()
            if maker.isinstance(child, 'Notebook'):
                child.configure()

    def create_group(self, name):
        """Create a new group"""
        if name not in self.groups:
            dbg('Terminator::create_group: registering group %s' % name)
            self.groups.append(name)

    def closegroupedterms(self, group):
        """Close all terminals in a group"""
        for terminal in self.terminals[:]:
            if terminal.group == group:
                terminal.close()

    def group_hoover(self):
        """Clean out unused groups"""

        if self.config['autoclean_groups']:
            inuse = []
            todestroy = []

            for terminal in self.terminals:
                if terminal.group:
                    if not terminal.group in inuse:
                        inuse.append(terminal.group)

            for group in self.groups:
                if not group in inuse:
                    todestroy.append(group)

            dbg('Terminator::group_hoover: %d groups, hoovering %d' %
                    (len(self.groups), len(todestroy)))
            for group in todestroy:
                self.groups.remove(group)

    def group_emit(self, terminal, group, type, event):
        """Emit to each terminal in a group"""
        dbg('Terminator::group_emit: emitting a keystroke for group %s' %
                group)
        for term in self.terminals:
            if term != terminal and term.group == group:
                term.vte.emit(type, event)

    def all_emit(self, terminal, type, event):
        """Emit to all terminals"""
        for term in self.terminals:
            if term != terminal:
                term.vte.emit(type, event)

    def do_enumerate(self, widget, pad):
        """Insert the number of each terminal in a group, into that terminal"""
        if pad:
            numstr = '%0'+str(len(str(len(self.terminals))))+'d'
        else:
            numstr = '%d'

        terminals = []
        for window in self.windows:
            containers, win_terminals = enumerate_descendants(window)
            terminals.extend(win_terminals)

        for term in self.get_target_terms(widget):
            idx = terminals.index(term)
            term.feed(numstr % (idx + 1))

    def get_sibling_terms(self, widget):
        termset = []
        for term in self.terminals:
            if term.group == widget.group:
                termset.append(term)
        return(termset)

    def get_target_terms(self, widget):
        """Get the terminals we should currently be broadcasting to"""
        if self.groupsend == self.groupsend_type['all']:
            return(self.terminals)
        elif self.groupsend == self.groupsend_type['group']:
            if widget.group != None:
                return(self.get_sibling_terms(widget))
        return([widget])

    def get_focussed_terminal(self):
        """iterate over all the terminals to find which, if any, has focus"""
        for terminal in self.terminals:
            if terminal.flags()&gtk.HAS_FOCUS:
                return(terminal)
        return(None)

    def focus_changed(self, widget):
        """We just moved focus to a new terminal"""
        for terminal in self.terminals:
            terminal.titlebar.update(widget)
        return

    def focus_left(self, widget):
        self.last_focused_term=widget

    def describe_layout(self):
        """Describe our current layout"""
        layout = {}
        count = 0
        for window in self.windows:
            parent = ''
            count = window.describe_layout(count, parent, layout, 0)

        return(layout)

# vim: set expandtab ts=4 sw=4:
