# OCFS2Console - GUI frontend for OCFS2 management and debugging
# Copyright (C) 2002, 2005 Oracle.  All rights reserved.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY 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, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 021110-1307, USA.

import gtk
import gobject
import pango

import ocfs2

try:
    IdleBase = gobject.Idle
except AttributeError:
    import gidle
    IdleBase = gidle.Idle

from guiutil import set_props

from ls import fields

INFO_LABEL_FONT = pango.FontDescription('monospace')

(
    COLUMN_NAME,
    COLUMN_INFO_OBJECT,
    COLUMN_ICON,
    COLUMN_ITALIC
) = range(4)

STOCK_LOADING = gtk.STOCK_REFRESH
STOCK_EMPTY = gtk.STOCK_STOP
STOCK_ERROR = gtk.STOCK_DIALOG_ERROR

try:
    STOCK_FILE = gtk.STOCK_FILE
except AttributeError:
    STOCK_FILE = gtk.STOCK_NEW

try:
    STOCK_DIRECTORY = gtk.STOCK_DIRECTORY
except AttributeError:
    STOCK_DIRECTORY = gtk.STOCK_OPEN

INVALID_DENTRY = 'poop'

class InfoLabel(gtk.Label):
    def __init__(self, field_type):
        gtk.Label.__init__(self)
        self.set_selectable(True)

        self.field_type = field_type

        if field_type.right_justify:
             set_props(self, xalign=1.0)
        else:
             set_props(self, xalign=0.0)

        self.modify_font(INFO_LABEL_FONT)

        if hasattr(field_type, 'width_chars'):
            context = self.get_pango_context()

            desc = INFO_LABEL_FONT.copy()
            desc.set_size(context.get_font_description().get_size())

            metrics = context.get_metrics(desc, context.get_language())

            char_width = metrics.get_approximate_char_width()
            digit_width = metrics.get_approximate_digit_width()
            char_pixels = pango.PIXELS(max(char_width, digit_width))

            self.set_size_request(char_pixels * field_type.width_chars, -1)

    def update(self, dentry, dinode):
        field = self.field_type(dentry, dinode)
        self.set_text(field.text)

    def clear(self):
        self.set_text('')

class Browser(gtk.VBox):
    def __init__(self, device=None):
        self.device = device

        gtk.VBox.__init__(self, spacing=4)

        label = gtk.Label('/')
        set_props(label, xalign=0.0,
                         selectable=True,
                         wrap=True)
        self.pack_start(label, expand=False)

        self.path_label = label

        self.scrl_win = gtk.ScrolledWindow()
        self.scrl_win.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
        self.add(self.scrl_win)

        self.make_dentry_store()
        self.make_file_view()
        self.make_ls_fields()

        self.connect('destroy', self.destroy_handler)

        self.refresh()

    def make_dentry_store(self):
        def tree_compare(store, a, b):
            d1 = self.get_dentry(store, a)
            d2 = self.get_dentry(store, b)

            if d1 is d2:
                return 0
            elif d1 is INVALID_DENTRY:
                return 1
            elif d2 is INVALID_DENTRY:
                return -1
            elif d1 and not d2:
                return 1
            elif not d1 and d2:
                return -1
            elif d1.file_type != d2.file_type:
                if d1.file_type == ocfs2.FT_DIR:
                    return -1
                elif d2.file_type == ocfs2.FT_DIR:
                    return 1
                else:
                    return cmp(d1.name, d2.name)
            else:
                return cmp(d1.name, d2.name)

        self.store = gtk.TreeStore(str, gobject.TYPE_PYOBJECT,
                                   str, gobject.TYPE_BOOLEAN)

        self.store.set_sort_func(COLUMN_NAME, tree_compare)
        self.store.set_sort_column_id(COLUMN_NAME, gtk.SORT_ASCENDING)

    def make_file_view(self):
        tv = gtk.TreeView(self.store)
        self.scrl_win.add(tv)

        set_props(tv, headers_visible=False)

        column = gtk.TreeViewColumn()

        renderer = gtk.CellRendererPixbuf()
        column.pack_start(renderer, expand=False)
        column.set_attributes(renderer, stock_id=COLUMN_ICON)

        renderer = gtk.CellRendererText()
        renderer.set_property('style', pango.STYLE_ITALIC)
        column.pack_start(renderer, expand=True)
        column.set_attributes(renderer, text=COLUMN_NAME,
                                        style_set=COLUMN_ITALIC)

        tv.append_column(column)

        tv.connect('test_expand_row', self.tree_expand_row)
        tv.connect('test_collapse_row', self.tree_collapse_row)

        sel = tv.get_selection()
        sel.connect('changed', self.select_dentry)

    def make_ls_fields(self):
        table = gtk.Table(rows=2, columns=7)
        set_props(table, row_spacing=4,
                         column_spacing=4,
                         border_width=4)
        self.pack_end(table, expand=False, fill=False)

        self.info_labels = []

        for column, field in enumerate(fields):
            label = gtk.Label(field.label)

            if field.right_justify:
                set_props(label, xalign=1.0)
            else:
                set_props(label, xalign=0.0)

            xoptions = yoptions = gtk.FILL
            xpadding = 2

            table.attach(label, column, column + 1, 0, 1,
                         xoptions, yoptions, xpadding)

            label = InfoLabel(field)
            table.attach(label, column, column + 1, 1, 2,
                         xoptions, yoptions, xpadding)

            self.info_labels.append(label)

    def destroy_handler(self, obj):
        self.cleanup()

    def make_dentry_node(self, dentry, stock_id, parent=None):
        return self.store.append(parent, (dentry.name, dentry, stock_id, False))

    def make_file_node(self, dentry, parent=None):
        self.make_dentry_node(dentry, STOCK_FILE, parent)

    def make_dir_node(self, dentry, parent=None):
        iter = self.make_dentry_node(dentry, STOCK_DIRECTORY, parent)
        self.store.append(iter, ('.', dentry, None, False))

    def make_loading_node(self, parent=None):
        self.store.append(parent, ('Loading...', None, STOCK_LOADING, True))

    def make_empty_node(self, parent=None):
        self.store.append(parent, ('Empty', None, STOCK_EMPTY, True))

    def make_error_node(self, parent=None):
        self.store.append(parent, ('Error', INVALID_DENTRY, STOCK_ERROR, True))

    def cleanup(self):
        if hasattr(self, 'levels'):
            for level in self.levels:
                level.destroy()

        self.levels = []

    def refresh(self):
        self.cleanup()

        self.store.clear()

        self.fs = None

        if self.device:
            try:
                self.fs = ocfs2.Filesystem(self.device)
            except ocfs2.error:
                self.make_error_node()

            if self.fs:
                self.add_level()
        else:
            self.make_empty_node()

    def add_level(self, dentry=None, parent=None):
        if parent:
            iter = self.store.iter_children(parent)

            name = self.store[iter][COLUMN_NAME]
            if name != '.':
                return

            del self.store[iter]

        try:
            diriter = self.fs.iterdir(dentry)
        except ocfs2.error:
            self.make_error_node(parent)
            return

        self.make_loading_node(parent)

        level = TreeLevel(diriter, dentry, parent)
        self.levels.append(level)

        if parent: 
            self.store[parent][COLUMN_INFO_OBJECT] = level
      
        level.set_callback(self.populate_level, level)
        level.attach()

    def populate_level(self, level):
        try:
            dentry = level.diriter.next()
        except (StopIteration, ocfs2.error), e:
             self.destroy_level(level, isinstance(e, ocfs2.error))
             return False

        if dentry.file_type == ocfs2.FT_DIR:
            self.make_dir_node(dentry, level.parent)
        else:
            self.make_file_node(dentry, level.parent)

        return True

    def destroy_level(self, level, error=False):
        if error:
            self.make_error_node(level.parent)
        else:
            children = self.store.iter_n_children(level.parent)

            if children < 2:
                self.make_empty_node(level.parent)

        if level.parent:
            self.store[level.parent][COLUMN_INFO_OBJECT] = level.dentry

            # Argh, ancient pygtk can't handle None being passed to
            # iter_children
            iter = self.store.iter_children(level.parent)
        else:
            iter = self.store.get_iter_first()

        self.store.remove(iter)

        self.levels.remove(level)

        del level.diriter

    def tree_expand_row(self, tv, iter, path):
        info_obj = self.store[iter][COLUMN_INFO_OBJECT]

        if isinstance(info_obj, TreeLevel):
            level.collapsed = False
            level.foreground(level)
        else:
            self.add_level(info_obj, iter)

    def tree_collapse_row(self, tv, iter, path):
        info_obj = self.store[iter][COLUMN_INFO_OBJECT]

        if isinstance(info_obj, TreeLevel):
            level = info_obj

            level.collapsed = True
            level.background()

    def select_dentry(self, sel):
        store, iter = sel.get_selected()

        if store and iter:
            dentry = self.get_dentry(store, iter)
        else:
            dentry = None

        if dentry:
            self.display_dentry(dentry)
        else:
            self.display_clear()
            if iter:
                iter = store.iter_parent(iter)

        self.path_label.set_text(self.get_fs_path(store, iter))

    def display_dentry(self, dentry):
        dinode = self.fs.read_cached_inode(dentry.inode)

        for label in self.info_labels:
            label.update(dentry, dinode)

    def display_clear(self):
        for label in self.info_labels:
            label.clear()

    def get_dentry(self, store, iter):
        info_obj = store[iter][COLUMN_INFO_OBJECT]

        if isinstance(info_obj, ocfs2.DirEntry):
            return info_obj
        elif isinstance(info_obj, TreeLevel):
            return info_obj.dentry
        else:
            return None

    def get_fs_path(self, store, iter):
        parts = []

        while iter:
            dentry = self.get_dentry(store, iter)
            parts.append(dentry.name)
            iter = store.iter_parent(iter)

        parts.reverse()

        return '/' + '/'.join(parts)

class TreeLevel(IdleBase):
    def __init__(self, diriter, dentry=None, parent=None):
        IdleBase.__init__(self)

        self.diriter = diriter
        self.dentry = dentry

        if parent:
            self.parent = parent.copy()
        else:
            self.parent = None

        self.collapsed = False

    def foreground(self):
        if not self.collapsed:
            self.priority = gobject.PRIORITY_DEFAULT_IDLE

    def background(self):
        self.priority = gobject.PRIORITY_LOW

def main():
    import sys

    def dummy(*args):
        gtk.main_quit()

    window = gtk.Window()
    window.set_default_size(400, 300)
    window.connect('delete_event', dummy)

    browser = Browser(sys.argv[1])
    window.add(browser)

    window.show_all()

    gtk.main()

if __name__ == '__main__':
    main()
