# -*- coding: utf-8 -*-
# DLV!sual - A very simple GUI frontend to DLV
# Copyright (c) 2009 Thomas Perl <thp@thpinfo.com>
# Initial release: 2009-11-08
# Website: http://thpinfo.com/2009/dlvisual/
#
# 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 3 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, see <http://www.gnu.org/licenses/>.


# some remarks:
# - put this in the same folder as all your DLV files (.dl, .hyp, .obs, etc..)
# - be sure to have the "dlv" utility in your $PATH
# - DLV will be run every time the selected files change (see MONITOR_FILES)
# - regex-based filtering is possible using the entry box
# - results in UPPERCASE are colored in RED
# - multiple answer sets are separated by a separator line in the treeview

# known bugs:
# - file list is not updated if directory contents changes (-> new files)
# - no support for frontends (diagnosis, etc..) so far

import cgi
import glob
import gobject
import gtk
import os
import pango
import re
import subprocess
import sys

# Which editor should be used in the file list (if ALLOW_EDITS is enabled)
ALLOW_EDITS = True
EDITOR = 'gvim'

# Should files be monitored for changes every second?
MONITOR_FILES = True

class FileList(gtk.ListStore):
    C_USE, C_MARKUP, C_FILENAME, C_MODIFIED = range(4)
    FILTER = ('*.dl', '*.hyp', '*.obs')

    def __init__(self, dir=''):
        gtk.ListStore.__init__(self, bool, str, str, long)
        self._dir = dir
        self.reload()

    def reload(self):
        self.clear()
        for filter in self.FILTER:
            match = os.path.join(self._dir, filter)
            for file in sorted(glob.glob(match)):
                self.add_file(file)

    def add_file(self, filename):
        selected = False
        # let the user focus on the files names without the extension..
        markup = '%s<span foreground="#aaa">%s</span>' % \
            tuple([cgi.escape(x) for x in os.path.splitext(os.path.basename(filename))])
        modified = os.stat(filename).st_mtime
        self.append([selected, markup, filename, modified])

    def check_files_changed(self):
        changed = False
        it = self.get_iter_first()
        while it is not None:
            selected = self.get_value(it, self.C_USE)
            modified = self.get_value(it, self.C_MODIFIED)
            filename = self.get_value(it, self.C_FILENAME)
            try:
                new_modified = os.stat(filename).st_mtime
            except:
                # ignore for now...
                continue
            if new_modified > modified and selected:
                self.set_value(it, self.C_MODIFIED, new_modified)
                print >>sys.stderr, filename, 'has changed'
                changed = True
            it = self.iter_next(it)
        return changed

    def get_selected(self):
        for use, markup, filename, modified in self:
            if use:
                yield filename

class DLV(gtk.ListStore):
    C_MARKUP, C_TEXT = range(2)

    def __init__(self):
        gtk.ListStore.__init__(self, str, str)
        self._options = []

    def execute(self, files):
        self.clear()
        if len(files):
            cmdline = ['dlv', '-silent']+list(files)+self._options
            p = subprocess.Popen(cmdline, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
            stdout, stderr = p.communicate()
        else:
            cmdline = ['please select files']
            stdout, stderr = '', ''


        if 'error' in stderr.lower():
            dlg = gtk.MessageDialog(buttons=gtk.BUTTONS_OK, message_format=cgi.escape(stderr))
            dlg.run()
            dlg.destroy()

        # tadaaaa! this is the magic line that takes the DLV output and splits it into a list of answer sets
        result = [[match.group(0) for match in re.finditer(r'\w+(\([^)]+\))?', line) if match] for line in stdout.splitlines()]

        if not result:
            pass
        elif not result[0]:
            pass
        else:
            for idx, answer_set in enumerate(result):
                for item in answer_set:
                    self.add(item)
                # add row separators for multiple answer sets
                if idx+1 < len(result):
                    self.split()

        return ' '.join(cmdline)

    def set_options(self, options):
        self._options = options

    def add(self, item):
        match = re.match(r'(\w+)\(([^)]*)\)', item)
        if match is None:
            markup = '<b>%s</b>' % cgi.escape(item)
        else:
            name, args = match.groups()
            args_markup = ','.join('<span foreground="#0a0">%s</span>'% cgi.escape(x) for x in args.split(','))
            markup = '<b>%s</b>(%s)' % (cgi.escape(name), args_markup)
        if item.lower() != item:
            # mark things in UPPERCASE with red color
            markup = '<span color="#f00">%s</span>' % markup
        self.append([markup, item])

    def split(self):
        self.append(['', ''])


class MainWindow(gtk.Window):
    def __init__(self, filelist, dlv):
        gtk.Window.__init__(self)
        self.set_default_size(700, 500)
        self.set_title('DLV!sual')
        self._filelist = filelist
        self._dlv = dlv
        vbox = gtk.VBox()
        self.add(vbox)
        hbox = gtk.HBox()
        hbox.set_spacing(6)
        hbox.set_border_width(6)
        hbox.pack_start(gtk.Label('DLV options:'), expand=False)
        self.entry_options = gtk.Entry()
        self.entry_options.set_text('-N=60')
        hbox.add(self.entry_options)
        button = gtk.Button('Recalculate')
        button.connect('clicked', self.on_run_dlv)
        hbox.pack_start(button, expand=False)
        vbox.pack_start(hbox, expand=False)
        paned = gtk.HPaned()
        paned.add1(self._create_filelist())
        paned.add2(self._create_output())
        paned.set_position(200)
        vbox.add(paned)
        self.label_command_line = gtk.Label()
        self.label_command_line.set_ellipsize(pango.ELLIPSIZE_END)
        self.label_command_line.set_padding(6, 6)
        self.label_command_line.set_alignment(0.0, 0.5)
        self.label_command_line.modify_font(pango.FontDescription('monospace'))
        self.label_command_line.set_selectable(True)
        vbox.pack_start(self.label_command_line, expand=False)
        self.show_all()
        if MONITOR_FILES:
            gobject.timeout_add(1000, self._check_files_changed)

    def _check_files_changed(self):
        if self._filelist.check_files_changed():
            self.on_run_dlv(None)
        return True

    def _create_filelist(self):
        sw = gtk.ScrolledWindow()
        sw.set_shadow_type(gtk.SHADOW_IN)
        sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
        tv = gtk.TreeView()
        def on_row_activated(tv, path, col):
            model = tv.get_model()
            it = model.get_iter(path)
            filename = model.get_value(it, FileList.C_FILENAME)
            subprocess.Popen([EDITOR, filename])
        if ALLOW_EDITS:
            tv.connect('row-activated', on_row_activated)
        crtoggle = gtk.CellRendererToggle()
        def on_toggled(crt, path):
            model = tv.get_model()
            it = model.get_iter(path)
            model.set_value(it, 0, not model.get_value(it, 0))
            self.on_run_dlv(None)
        crtoggle.connect('toggled', on_toggled)
        crtoggle.set_property('activatable', True)
        tv.append_column(gtk.TreeViewColumn('Use', crtoggle, active=0))
        tv.append_column(gtk.TreeViewColumn('Filename', gtk.CellRendererText(), markup=1))
        tv.set_model(self._filelist)
        sw.add(tv)
        return sw

    def _create_output(self):
        sw = gtk.ScrolledWindow()
        sw.set_shadow_type(gtk.SHADOW_IN)
        sw.set_policy(gtk.POLICY_AUTOMATIC, gtk.POLICY_AUTOMATIC)
        tv = gtk.TreeView()
        def is_row_separator(model, iter):
            return model.get_value(iter, DLV.C_TEXT) == ''
        tv.set_row_separator_func(is_row_separator)
        tv.append_column(gtk.TreeViewColumn('', gtk.CellRendererText(), markup=0))
        _filter = self._dlv.filter_new()
        entry_filter = gtk.Entry()
        def _is_visible(model, iter):
            text = entry_filter.get_text()
            value = model.get_value(iter, 1)
            if text and value:
                try:
                    entry_filter.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse('#ffffff'))
                    return re.search(text, value, re.I) is not None
                except:
                    entry_filter.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse('#ff0000'))
                    return True
            else:
                entry_filter.modify_base(gtk.STATE_NORMAL, gtk.gdk.color_parse('#ffffff'))
                return True
        _filter.set_visible_func(_is_visible)
        tv.set_model(_filter)
        tv.set_headers_visible(False)
        sw.add(tv)
        vbox = gtk.VBox()
        def _on_filter_changed(entry):
            _filter.refilter()
        entry_filter.connect('changed', _on_filter_changed)
        hbox = gtk.HBox()
        hbox.set_spacing(6)
        hbox.set_border_width(6)
        hbox.pack_start(gtk.Label('Filter (regex):'), expand=False)
        hbox.pack_start(entry_filter)
        vbox.pack_start(hbox, expand=False)
        vbox.add(sw)
        return vbox

    def on_run_dlv(self, button):
        self._dlv.set_options(self.entry_options.get_text().split())
        self.label_command_line.set_text(self._dlv.execute(list(self._filelist.get_selected())))

# model classes
filelist = FileList()
dlv = DLV()

main_window = MainWindow(filelist, dlv)
main_window.connect('destroy', gtk.main_quit)

gtk.main()


